diff --git a/doc/img/Radiosonde_plugin_sondehub_settings.png b/doc/img/Radiosonde_plugin_sondehub_settings.png
new file mode 100644
index 000000000..102fb2e79
Binary files /dev/null and b/doc/img/Radiosonde_plugin_sondehub_settings.png differ
diff --git a/plugins/feature/radiosonde/CMakeLists.txt b/plugins/feature/radiosonde/CMakeLists.txt
index c9d5a22b8..c8315a826 100644
--- a/plugins/feature/radiosonde/CMakeLists.txt
+++ b/plugins/feature/radiosonde/CMakeLists.txt
@@ -24,10 +24,13 @@ if(NOT SERVER_MODE)
radiosondegui.cpp
radiosondegui.ui
radiosonde.qrc
+ radiosondefeedsettingsdialog.cpp
+ radiosondefeedsettingsdialog.ui
)
set(radiosonde_HEADERS
${radiosonde_HEADERS}
radiosondegui.h
+ radiosondefeedsettingsdialog.h
)
set(TARGET_NAME featureradiosonde)
diff --git a/plugins/feature/radiosonde/radiosondefeedsettingsdialog.cpp b/plugins/feature/radiosonde/radiosondefeedsettingsdialog.cpp
new file mode 100644
index 000000000..11d5759c6
--- /dev/null
+++ b/plugins/feature/radiosonde/radiosondefeedsettingsdialog.cpp
@@ -0,0 +1,48 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "radiosondefeedsettingsdialog.h"
+
+RadiosondeFeedSettingsDialog::RadiosondeFeedSettingsDialog(RadiosondeSettings *settings, QWidget* parent) :
+ QDialog(parent),
+ ui(new Ui::RadiosondeFeedSettingsDialog),
+ m_settings(settings)
+{
+ ui->setupUi(this);
+
+ ui->callsign->setText(m_settings->m_callsign);
+ ui->antenna->setText(m_settings->m_antenna);
+ ui->displayPosition->setChecked(m_settings->m_displayPosition);
+ ui->mobile->setChecked(m_settings->m_mobile);
+ ui->email->setText(m_settings->m_email);
+}
+
+RadiosondeFeedSettingsDialog::~RadiosondeFeedSettingsDialog()
+{
+ delete ui;
+}
+
+void RadiosondeFeedSettingsDialog::accept()
+{
+ m_settings->m_callsign = ui->callsign->text();
+ m_settings->m_antenna = ui->antenna->text();
+ m_settings->m_displayPosition = ui->displayPosition->isChecked();
+ m_settings->m_mobile = ui->mobile->isChecked();
+ m_settings->m_email = ui->email->text();
+
+ QDialog::accept();
+}
diff --git a/plugins/feature/radiosonde/radiosondefeedsettingsdialog.h b/plugins/feature/radiosonde/radiosondefeedsettingsdialog.h
new file mode 100644
index 000000000..4953935b7
--- /dev/null
+++ b/plugins/feature/radiosonde/radiosondefeedsettingsdialog.h
@@ -0,0 +1,42 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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_FEATURE_RADIOSONDEFEEDSETTINGSDIALOG_H
+#define INCLUDE_FEATURE_RADIOSONDEFEEDSETTINGSDIALOG_H
+
+#include "ui_radiosondefeedsettingsdialog.h"
+#include "radiosondesettings.h"
+
+class RadiosondeFeedSettingsDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit RadiosondeFeedSettingsDialog(RadiosondeSettings *settings, QWidget* parent = 0);
+ ~RadiosondeFeedSettingsDialog();
+
+private:
+
+private slots:
+ void accept();
+
+private:
+ Ui::RadiosondeFeedSettingsDialog* ui;
+ RadiosondeSettings *m_settings;
+
+};
+
+#endif // INCLUDE_FEATURE_RADIOSONDEFEEDSETTINGSDIALOG_H
diff --git a/plugins/feature/radiosonde/radiosondefeedsettingsdialog.ui b/plugins/feature/radiosonde/radiosondefeedsettingsdialog.ui
new file mode 100644
index 000000000..377b578ca
--- /dev/null
+++ b/plugins/feature/radiosonde/radiosondefeedsettingsdialog.ui
@@ -0,0 +1,155 @@
+
+
+ RadiosondeFeedSettingsDialog
+
+
+
+ 0
+ 0
+ 441
+ 211
+
+
+
+
+ Liberation Sans
+ 9
+
+
+
+ SondeHub Feed Settings
+
+
+ -
+
+
+ SondeHub Feed Settings
+
+
+
-
+
+
+ Callsign
+
+
+
+ -
+
+
+ Callsign of feeder / uploader
+
+
+
+ -
+
+
+ E-mail
+
+
+
+ -
+
+
+ E-mail of feeder / uploader
+
+
+
+ -
+
+
+ Display position
+
+
+
+ -
+
+
+ Check to publically display receiver position on SondeHub map
+
+
+
+
+
+
+ -
+
+
+ Mobile
+
+
+
+ -
+
+
+ Check to indicate if receiver is mobile (E.g. chase car)
+
+
+
+
+
+
+ -
+
+
+ Antenna
+
+
+
+ -
+
+
+ Description of antenna
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ RadiosondeFeedSettingsDialog
+ accept()
+
+
+ 257
+ 31
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ RadiosondeFeedSettingsDialog
+ reject()
+
+
+ 325
+ 31
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/plugins/feature/radiosonde/radiosondegui.cpp b/plugins/feature/radiosonde/radiosondegui.cpp
index 533dcfb2a..585a6707a 100644
--- a/plugins/feature/radiosonde/radiosondegui.cpp
+++ b/plugins/feature/radiosonde/radiosondegui.cpp
@@ -29,12 +29,15 @@
#include "gui/decimaldelegate.h"
#include "gui/tabletapandhold.h"
#include "gui/dialogpositioner.h"
+#include "gui/crightclickenabler.h"
#include "mainwindow.h"
#include "device/deviceuiset.h"
+#include "device/deviceapi.h"
#include "ui_radiosondegui.h"
#include "radiosonde.h"
#include "radiosondegui.h"
+#include "radiosondefeedsettingsdialog.h"
#include "SWGMapItem.h"
@@ -153,6 +156,8 @@ RadiosondeGUI::RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, F
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
+ m_sondeHub = SondeHub::create();
+
// Intialise chart
ui->chart->setRenderHint(QPainter::Antialiasing);
@@ -180,14 +185,20 @@ RadiosondeGUI::RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, F
TableTapAndHold *tableTapAndHold = new TableTapAndHold(ui->radiosondes);
connect(tableTapAndHold, &TableTapAndHold::tapAndHold, this, &RadiosondeGUI::customContextMenuRequested);
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LATITUDE, new DecimalDelegate(5));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LONGITUDE, new DecimalDelegate(5));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_ALTITUDE, new DecimalDelegate(1));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_SPEED, new DecimalDelegate(1));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_VERTICAL_RATE, new DecimalDelegate(1));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_HEADING, new DecimalDelegate(1));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_ALT_MAX, new DecimalDelegate(1));
- ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LAST_UPDATE, new DateTimeDelegate());
+ CRightClickEnabler *feedRightClickEnabler = new CRightClickEnabler(ui->feed);
+ connect(feedRightClickEnabler, &CRightClickEnabler::rightClick, this, &RadiosondeGUI::feedSelect);
+
+ // Get updated when position changes
+ connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &RadiosondeGUI::preferenceChanged);
+
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LATITUDE, new DecimalDelegate(5, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LONGITUDE, new DecimalDelegate(5, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_ALTITUDE, new DecimalDelegate(1, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_SPEED, new DecimalDelegate(1, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_VERTICAL_RATE, new DecimalDelegate(1, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_HEADING, new DecimalDelegate(1, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_ALT_MAX, new DecimalDelegate(1, ui->radiosondes));
+ ui->radiosondes->setItemDelegateForColumn(RADIOSONDE_COL_LAST_UPDATE, new DateTimeDelegate("yyyy/MM/dd hh:mm:ss", ui->radiosondes));
m_settings.setRollupState(&m_rollupState);
@@ -201,9 +212,11 @@ RadiosondeGUI::RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, F
RadiosondeGUI::~RadiosondeGUI()
{
+ disconnect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &RadiosondeGUI::preferenceChanged);
// Remove from map and free memory
on_deleteAll_clicked();
delete ui;
+ delete m_sondeHub;
}
void RadiosondeGUI::setWorkspaceIndex(int index)
@@ -241,9 +254,13 @@ void RadiosondeGUI::displaySettings()
ui->y1->setCurrentIndex((int)m_settings.m_y1);
ui->y2->setCurrentIndex((int)m_settings.m_y2);
+ ui->feed->setChecked(m_settings.m_feedEnabled);
+
getRollupContents()->restoreState(m_rollupState);
blockApplySettings(false);
getRollupContents()->arrangeRollups();
+
+ updatePosition();
}
void RadiosondeGUI::onMenuDialogCalled(const QPoint &p)
@@ -640,6 +657,20 @@ void RadiosondeGUI::updateRadiosondes(RS41Frame *message, QDateTime dateTime)
}
plotChart();
+
+ if (m_sondeHub && m_settings.m_feedEnabled)
+ {
+ // Feed to SondeHub
+ m_sondeHub->upload(
+ MainCore::instance()->getSettings().getStationName(),
+ dateTime,
+ message,
+ &radiosonde->m_subframe,
+ MainCore::instance()->getSettings().getLatitude(),
+ MainCore::instance()->getSettings().getLongitude(),
+ MainCore::instance()->getSettings().getAltitude()
+ );
+ }
}
void RadiosondeGUI::on_radiosondes_itemSelectionChanged()
@@ -894,4 +925,83 @@ void RadiosondeGUI::makeUIConnections()
QObject::connect(ui->y1, qOverload(&QComboBox::currentIndexChanged), this, &RadiosondeGUI::on_y1_currentIndexChanged);
QObject::connect(ui->y2, qOverload(&QComboBox::currentIndexChanged), this, &RadiosondeGUI::on_y2_currentIndexChanged);
QObject::connect(ui->deleteAll, &QPushButton::clicked, this, &RadiosondeGUI::on_deleteAll_clicked);
+ QObject::connect(ui->feed, &ButtonSwitch::clicked, this, &RadiosondeGUI::on_feed_clicked);
+}
+
+void RadiosondeGUI::on_feed_clicked(bool checked)
+{
+ m_settings.m_feedEnabled = checked;
+ m_settingsKeys.append("feedEnabled");
+ applySettings();
+}
+
+// Show feed dialog
+void RadiosondeGUI::feedSelect(const QPoint& p)
+{
+ RadiosondeFeedSettingsDialog dialog(&m_settings);
+ dialog.move(p);
+ new DialogPositioner(&dialog, false);
+
+ if (dialog.exec() == QDialog::Accepted)
+ {
+ m_settingsKeys.append("callsign");
+ m_settingsKeys.append("antenna");
+ m_settingsKeys.append("displayPosition");
+ m_settingsKeys.append("mobile");
+ m_settingsKeys.append("email");
+ applySettings();
+ updatePosition();
+ }
+}
+
+// Get names of devices with radiosonde demods, for SondeHub Radio string
+QStringList RadiosondeGUI::getRadios()
+{
+ MainCore *mainCore = MainCore::instance();
+ QStringList deviceList;
+ AvailableChannelOrFeatureList channels = mainCore->getAvailableChannels({"sdrangel.channel.radiosondedemod"});
+
+ for (const auto& channel : channels)
+ {
+ DeviceAPI *device = mainCore->getDevice(channel.m_index);
+ if (device)
+ {
+ QString name = device->getHardwareId();
+
+ if (!deviceList.contains(name)) {
+ deviceList.append(name);
+ }
+ }
+ }
+
+ return deviceList;
+}
+
+void RadiosondeGUI::updatePosition()
+{
+ if (m_sondeHub && m_settings.m_displayPosition)
+ {
+ float stationLatitude = MainCore::instance()->getSettings().getLatitude();
+ float stationLongitude = MainCore::instance()->getSettings().getLongitude();
+ float stationAltitude = MainCore::instance()->getSettings().getAltitude();
+
+ m_sondeHub->updatePosition(
+ m_settings.m_callsign,
+ stationLatitude,
+ stationLongitude,
+ stationAltitude,
+ getRadios().join(" "),
+ m_settings.m_antenna,
+ m_settings.m_email,
+ m_settings.m_mobile
+ );
+ }
+}
+
+void RadiosondeGUI::preferenceChanged(int elementType)
+{
+ Preferences::ElementType pref = (Preferences::ElementType)elementType;
+ if ((pref == Preferences::Latitude) || (pref == Preferences::Longitude) || (pref == Preferences::Altitude)) {
+ updatePosition();
+ }
}
diff --git a/plugins/feature/radiosonde/radiosondegui.h b/plugins/feature/radiosonde/radiosondegui.h
index 2d5a58d52..124a189ea 100644
--- a/plugins/feature/radiosonde/radiosondegui.h
+++ b/plugins/feature/radiosonde/radiosondegui.h
@@ -31,6 +31,7 @@
#include "feature/featuregui.h"
#include "util/messagequeue.h"
#include "util/radiosonde.h"
+#include "util/sondehub.h"
#include "settings/rollupstate.h"
#include "radiosondesettings.h"
@@ -101,6 +102,8 @@ private:
QMenu *radiosondesMenu; // Column select context menu
+ SondeHub *m_sondeHub;
+
explicit RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
virtual ~RadiosondeGUI();
@@ -121,6 +124,8 @@ private:
QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot);
void plotChart();
float getData(RadiosondeSettings::ChartData dataType, RadiosondeData *radiosonde, RS41Frame *message);
+ void updatePosition();
+ QStringList getRadios();
enum RadiosondeCol {
RADIOSONDE_COL_SERIAL,
@@ -157,6 +162,10 @@ private slots:
void on_y1_currentIndexChanged(int index);
void on_y2_currentIndexChanged(int index);
void on_deleteAll_clicked();
+ void on_feed_clicked(bool checked);
+ void feedSelect(const QPoint& p);
+ void preferenceChanged(int elementType);
+
};
#endif // INCLUDE_FEATURE_RADIOSONDEGUI_H_
diff --git a/plugins/feature/radiosonde/radiosondegui.ui b/plugins/feature/radiosonde/radiosondegui.ui
index 4dccbfa46..eee177b19 100644
--- a/plugins/feature/radiosonde/radiosondegui.ui
+++ b/plugins/feature/radiosonde/radiosondegui.ui
@@ -399,6 +399,23 @@
+ -
+
+
+ Enable feeding of received frames to SondeHub. Right click for settings.
+
+
+ ...
+
+
+
+ :/txon.png:/txon.png
+
+
+ true
+
+
+
-
@@ -419,6 +436,11 @@
+
+ ButtonSwitch
+ QToolButton
+
+
RollupContents
QWidget
diff --git a/plugins/feature/radiosonde/radiosondesettings.cpp b/plugins/feature/radiosonde/radiosondesettings.cpp
index 75b6d3309..c45c27b90 100644
--- a/plugins/feature/radiosonde/radiosondesettings.cpp
+++ b/plugins/feature/radiosonde/radiosondesettings.cpp
@@ -2,7 +2,7 @@
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2017, 2019-2020, 2022 Edouard Griffiths, F4EXB //
-// Copyright (C) 2021-2022 Jon Beniston, M7RCE //
+// Copyright (C) 2021-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 //
@@ -23,6 +23,7 @@
#include "util/simpleserializer.h"
#include "settings/serializable.h"
+#include "maincore.h"
#include "radiosondesettings.h"
@@ -53,6 +54,13 @@ void RadiosondeSettings::resetToDefaults()
m_y1 = ALTITUDE;
m_y2 = TEMPERATURE;
+ m_feedEnabled = false;
+ m_callsign = MainCore::instance()->getSettings().getStationName();
+ m_antenna = "";
+ m_displayPosition = false;
+ m_mobile = false;
+ m_email = "";
+
for (int i = 0; i < RADIOSONDES_COLUMNS; i++)
{
m_radiosondesColumnIndexes[i] = i;
@@ -81,6 +89,14 @@ QByteArray RadiosondeSettings::serialize() const
s.writeS32(12, m_workspaceIndex);
s.writeBlob(13, m_geometryBytes);
+ s.writeBool(14, m_feedEnabled);
+ s.writeString(15, m_callsign);
+ s.writeString(16, m_antenna);
+ s.writeBool(17, m_displayPosition);
+ s.writeBool(18, m_mobile);
+ s.writeString(19, m_email);
+
+
for (int i = 0; i < RADIOSONDES_COLUMNS; i++) {
s.writeS32(300 + i, m_radiosondesColumnIndexes[i]);
}
@@ -137,6 +153,13 @@ bool RadiosondeSettings::deserialize(const QByteArray& data)
d.readS32(12, &m_workspaceIndex, 0);
d.readBlob(13, &m_geometryBytes);
+ d.readBool(14, &m_feedEnabled, false);
+ d.readString(15, &m_callsign, MainCore::instance()->getSettings().getStationName());
+ d.readString(16, &m_antenna, "");
+ d.readBool(17, &m_displayPosition, false);
+ d.readBool(18, &m_mobile, false);
+ d.readString(19, &m_email, "");
+
for (int i = 0; i < RADIOSONDES_COLUMNS; i++) {
d.readS32(300 + i, &m_radiosondesColumnIndexes[i], i);
}
@@ -183,6 +206,24 @@ void RadiosondeSettings::applySettings(const QStringList& settingsKeys, const Ra
if (settingsKeys.contains("y2")) {
m_y2 = settings.m_y2;
}
+ if (settingsKeys.contains("feedEnabled")) {
+ m_feedEnabled = settings.m_feedEnabled;
+ }
+ if (settingsKeys.contains("callsign")) {
+ m_callsign = settings.m_callsign;
+ }
+ if (settingsKeys.contains("antenna")) {
+ m_antenna = settings.m_antenna;
+ }
+ if (settingsKeys.contains("displayPosition")) {
+ m_displayPosition = settings.m_displayPosition;
+ }
+ if (settingsKeys.contains("mobile")) {
+ m_mobile = settings.m_mobile;
+ }
+ if (settingsKeys.contains("email")) {
+ m_email = settings.m_email;
+ }
if (settingsKeys.contains("workspaceIndex")) {
m_workspaceIndex = settings.m_workspaceIndex;
}
@@ -233,6 +274,24 @@ QString RadiosondeSettings::getDebugString(const QStringList& settingsKeys, bool
if (settingsKeys.contains("y2") || force) {
ostr << " m_y2: " << m_y2;
}
+ if (settingsKeys.contains("feedEnabled") || force) {
+ ostr << " m_feedEnabled: " << m_feedEnabled;
+ }
+ if (settingsKeys.contains("callsign") || force) {
+ ostr << " m_callsign: " << m_callsign.toStdString();
+ }
+ if (settingsKeys.contains("antenna") || force) {
+ ostr << " m_antenna: " << m_antenna.toStdString();
+ }
+ if (settingsKeys.contains("displayPosition") || force) {
+ ostr << " m_displayPosition: " << m_displayPosition;
+ }
+ if (settingsKeys.contains("mobile") || force) {
+ ostr << " m_mobile: " << m_mobile;
+ }
+ if (settingsKeys.contains("email") || force) {
+ ostr << " m_email: " << m_email.toStdString();
+ }
if (settingsKeys.contains("workspaceIndex") || force) {
ostr << " m_workspaceIndex: " << m_workspaceIndex;
}
diff --git a/plugins/feature/radiosonde/radiosondesettings.h b/plugins/feature/radiosonde/radiosondesettings.h
index b54de58e2..cc9ffc914 100644
--- a/plugins/feature/radiosonde/radiosondesettings.h
+++ b/plugins/feature/radiosonde/radiosondesettings.h
@@ -2,7 +2,7 @@
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB //
-// Copyright (C) 2021-2022 Jon Beniston, M7RCE //
+// Copyright (C) 2021-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 //
@@ -29,7 +29,7 @@
class Serializable;
// Number of columns in the table
-#define RADIOSONDES_COLUMNS 16
+#define RADIOSONDES_COLUMNS 18
struct RadiosondeSettings
{
@@ -58,6 +58,13 @@ struct RadiosondeSettings
ChartData m_y1;
ChartData m_y2;
+ bool m_feedEnabled;
+ QString m_callsign;
+ QString m_antenna;
+ bool m_displayPosition;
+ bool m_mobile;
+ QString m_email;
+
int m_radiosondesColumnIndexes[RADIOSONDES_COLUMNS];
int m_radiosondesColumnSizes[RADIOSONDES_COLUMNS];
diff --git a/plugins/feature/radiosonde/readme.md b/plugins/feature/radiosonde/readme.md
index 46812851a..e9fc443a3 100644
--- a/plugins/feature/radiosonde/readme.md
+++ b/plugins/feature/radiosonde/readme.md
@@ -9,6 +9,8 @@ The chart can plot two data series vs time for the radiosonde selected in the ta
The Radiosonde feature can draw balloons objects on the [Map](../../feature/map/readme.md) feature in 2D and 3D.
+Received data can be forwarded to [SondeHub](https://sondehub.org/). Your location can be displayed on the SondeHub map, as either a stationary receiver or chase car.
+
Interface
![Radiosonde feature plugin GUI](../../../doc/img/Radiosonde_plugin.png)
@@ -49,6 +51,20 @@ To centre the map on an item in the table, double click in the Lat or Lon column
![Radiosonde on map](../../../doc/img/Radiosonde_plugin_map.png)
+Feeding Data to SondeHub
+
+Received radiosonde frames can be forwarded to [SondeHub](https://sondehub.org/) by clicking the Feed button.
+
+Right clicking the feed button opens the SondeHub Feed Settings dialog:
+
+![SondeHub settings dialog](../../../doc/img/Radiosonde_plugin_sondehub_settings.png)
+
+* Callsign should be your amateur callsign and indicates who the frames have been received by.
+* Enter your e-mail address. This isn't displayed on the SondeHub map.
+* Check display position if you would like your position displayed on the SondeHub map.
+* Check mobile to indicate that your receiver is mobile, and it will be displayed on the SondeHub map as a chase car. If unchecked, your receiver will be displayed as stationary with a green circle.
+* Antenna is a free text string you can use to describe your antenna. This will be displayed on the SondeHub map.
+
Attribution
* Hot-air-balloon icons created by Freepik - https://www.flaticon.com/free-icons/hot-air-balloon
diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt
index 8de90e62f..7f04a162e 100644
--- a/sdrbase/CMakeLists.txt
+++ b/sdrbase/CMakeLists.txt
@@ -269,6 +269,7 @@ set(sdrbase_SOURCES
util/simpleserializer.cpp
util/serialutil.cpp
util/solardynamicsobservatory.cpp
+ util/sondehub.cpp
#util/spinlock.cpp
util/spyserverlist.cpp
util/stix.cpp
@@ -528,6 +529,7 @@ set(sdrbase_HEADERS
util/simpleserializer.h
util/serialutil.h
util/solardynamicsobservatory.h
+ util/sondehub.h
#util/spinlock.h
util/spyserverlist.h
util/stix.h
diff --git a/sdrbase/util/radiosonde.cpp b/sdrbase/util/radiosonde.cpp
index 0ac5037f8..37eef99c3 100644
--- a/sdrbase/util/radiosonde.cpp
+++ b/sdrbase/util/radiosonde.cpp
@@ -163,8 +163,8 @@ void RS41Frame::decodeGPSPos(const QByteArray ba)
}
}
-// Find the water vapor saturation pressure for a given temperature.
-float waterVapourSaturationPressure(float tCelsius)
+// Find the water vapor saturation pressure for a given temperature (for tCelsius < 0C).
+static float waterVapourSaturationPressure(float tCelsius)
{
// Convert to Kelvin
float T = tCelsius + 273.15f;
@@ -187,7 +187,7 @@ float waterVapourSaturationPressure(float tCelsius)
return p / 100.0f;
}
-float calcT(int f, int f1, int f2, float r1, float r2, float *poly, float *cal)
+static float calcT(int f, int f1, int f2, float r1, float r2, float *poly, float *cal)
{
/*float g = (float)(f2-f1) / (r2-r1); // gain
float Rb = (f1*r2-f2*r1) / (float)(f2-f1); // offset
@@ -219,11 +219,11 @@ float calcT(int f, int f1, int f2, float r1, float r2, float *poly, float *cal)
return tCal;
}
-float calcU(int cInt, int cMin, int cMax, float c1, float c2, float T, float HT, float *capCal, float *matrixCal)
+static float calcU(int cInt, int cMin, int cMax, float c1, float c2, float T, float HT, float *capCal, float *matrixCal, float height, float *vectorPCal, float *matrixPCal)
{
- //qDebug() << "cInt " << cInt << " cMin " << cMin << " cMax " << cMax << " c1 " << c1 << " c2 " << c2 << " T " << T << " HT " << HT << " capCal[0] " << capCal[0] << " capCal[1] " << capCal[1];
- /*
- float a0 = 7.5f;
+ //qDebug() << "cInt " << cInt << " cMin " << cMin << " cMax " << cMax << " c1 " << c1 << " c2 " << c2 << " T " << T << " HT " << HT << " capCal[0] " << capCal[0] << " capCal[1] " << capCal[1] << "height" << height;
+
+ /*float a0 = 7.5f;
float a1 = 350.0f / capCal[0];
float fh = (cInt-cMin) / (float)(cMax-cMin);
float rh = 100.0f * (a1*fh - a0);
@@ -243,7 +243,7 @@ float calcU(int cInt, int cMin, int cMax, float c1, float c2, float T, float HT,
rh = -1.0;
}
- qDebug() << "RH old method: " << rh; */
+ qDebug() << "RH old method: " << rh;*/
// Convert integer measurement to scale factor
@@ -252,8 +252,32 @@ float calcU(int cInt, int cMin, int cMax, float c1, float c2, float T, float HT,
// Calculate capacitance (scale between two reference caps)
float cUncal = c1 + (c2 - c1) * s;
float cCal = (cUncal / capCal[0] - 1.0f) * capCal[1];
- float uUncal = 0.0f;
+
float t = (HT - 20.0f) / 180.0f;
+
+ // Calculate standard pressure at given height in hPa
+ float pressure = 1013.25f * expf(-1.18575919e-4f * height);
+
+ // Compensation for pressure
+ float p = pressure / 1000.0f;
+ float powc = 1.0f;
+ float sum = 0.0f;
+ for (int i = 0; i < 3; i++)
+ {
+ float l = 0.0f;
+ float powt = 1.0f;
+ for (int j = 0; j < 4; j++)
+ {
+ l += matrixPCal[4*i+j] * powt;
+ powt *= t;
+ }
+ float x = vectorPCal[i];
+ sum += l * (x * p / (1.0f + x * p) - x * powc / (1.0f + x));
+ powc *= cCal;
+ }
+ cCal -= sum;
+
+ float uUncal = 0.0f;
float f1 = 1.0f;
for (int i = 0; i < 7; i++)
{
@@ -267,16 +291,18 @@ float calcU(int cInt, int cMin, int cMax, float c1, float c2, float T, float HT,
}
// Adjust for difference in outside air temperature and the humidty sensor temperature
- float uCal = uUncal * waterVapourSaturationPressure(T) / waterVapourSaturationPressure(HT);
+ float uCal = uUncal * waterVapourSaturationPressure(HT) / waterVapourSaturationPressure(T);
// Ensure within range of 0..100%
uCal = std::min(100.0f, uCal);
uCal = std::max(0.0f, uCal);
+ //qDebug() << "RH new method" << uCal;
+
return uCal;
}
-float calcP(int f, int f1, int f2, float pressureTemp, float *cal)
+static float calcP(int f, int f1, int f2, float pressureTemp, float *cal)
{
// Convert integer measurement to scale factor
float s = (f-f1) / (float)(f2-f1);
@@ -434,6 +460,8 @@ void RS41Frame::calcHumidity(const RS41Subframe *subframe)
float c1, c2;
float capCal[2];
float calMatrix[7*6];
+ float pCalMatrix[12];
+ float pCalVector[3];
if (m_humidityMain == 0)
{
@@ -449,10 +477,13 @@ void RS41Frame::calcHumidity(const RS41Subframe *subframe)
m_humidityCalibrated = m_temperatureCalibrated && m_humidityTemperatureCalibrated && humidityCalibrated;
+ subframe->getHumidityPressureCal(pCalVector, pCalMatrix);
+
m_humidity = calcU(m_humidityMain, m_humidityRef1, m_humidityRef2,
c1, c2,
temperature, humidityTemperature,
- capCal, calMatrix);
+ capCal, calMatrix,
+ m_height, pCalVector, pCalMatrix);
// RS41 humidity resolution of 0.1%
m_humidityString = QString::number(m_humidity, 'f', 1);
@@ -638,12 +669,51 @@ bool RS41Subframe::getPressureCal(float *cal) const
}
}
+// Indicate if we have all the required humidity pressure calibration data
+bool RS41Subframe::hasHumidityPressureCal() const
+{
+ return m_subframeValid[0x2a] && m_subframeValid[0x2b] && m_subframeValid[0x2c]
+ && m_subframeValid[0x2d] && m_subframeValid[0x2e] && m_subframeValid[0x2f];
+}
+
+bool RS41Subframe::getHumidityPressureCal(float *vec, float *mat) const
+{
+ if (hasHumidityPressureCal())
+ {
+ for (int i = 0; i < 3; i++) {
+ vec[i] = getFloat(0x2a6 + i * 4);
+ }
+ for (int i = 0; i < 12; i++) {
+ mat[i] = getFloat(0x2ba + i * 4);
+ }
+ return true;
+ }
+ else
+ {
+ // Use default values - TODO: Need to obtain from inflight device
+ for (int i = 0; i < 3; i++) {
+ vec[i] = 0.0f;
+ }
+ for (int i = 0; i < 12; i++) {
+ mat[i] = 0.0f;
+ }
+ qDebug() << "hasHumidityPressureCal: false";
+ return false;
+ }
+}
+
// Get type of RS41. E.g. "RS41-SGP"
QString RS41Subframe::getType() const
{
- if (m_subframeValid[0x21] & m_subframeValid[0x22])
+ if (m_subframeValid[0x21] && m_subframeValid[0x22])
{
- return QString(m_subframe.mid(0x218, 10)).trimmed();
+ QByteArray bytes = m_subframe.mid(0x218, 10);
+
+ while ((bytes.size() > 0) && (bytes.back() == 0)) {
+ bytes.removeLast();
+ }
+
+ return QString(bytes).trimmed();
}
else
{
diff --git a/sdrbase/util/radiosonde.h b/sdrbase/util/radiosonde.h
index 54914d7c3..5e75c2627 100644
--- a/sdrbase/util/radiosonde.h
+++ b/sdrbase/util/radiosonde.h
@@ -113,11 +113,14 @@ public:
float getPressureFloat(const RS41Subframe *subframe);
QString getPressureString(const RS41Subframe *subframe);
+ bool isPressureCalibrated() const { return m_pressureCalibrated; }
float getTemperatureFloat(const RS41Subframe *subframe);
QString getTemperatureString(const RS41Subframe *subframe);
+ bool isTemperatureCalibrated() const { return m_temperatureCalibrated; }
float getHumidityTemperatureFloat(const RS41Subframe *subframe);
float getHumidityFloat(const RS41Subframe *subframe);
QString getHumidityString(const RS41Subframe *subframe);
+ bool isHumidityCalibrated() const { return m_humidityCalibrated; }
static RS41Frame* decode(const QByteArray ba);
static int getFrameLength(int frameType);
@@ -162,6 +165,8 @@ public:
bool getHumidityTempCal(float &r1, float &r2, float *poly, float *cal) const;
bool hasPressureCal() const;
bool getPressureCal(float *cal) const;
+ bool hasHumidityPressureCal() const;
+ bool getHumidityPressureCal(float *vec, float *mat) const;
QString getType() const;
QString getFrequencyMHz() const;
QString getBurstKillStatus() const;
diff --git a/sdrbase/util/sondehub.cpp b/sdrbase/util/sondehub.cpp
new file mode 100644
index 000000000..4f8cf90f2
--- /dev/null
+++ b/sdrbase/util/sondehub.cpp
@@ -0,0 +1,202 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "sondehub.h"
+#include "util/radiosonde.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+SondeHub::SondeHub()
+{
+ m_networkManager = new QNetworkAccessManager();
+ connect(m_networkManager, &QNetworkAccessManager::finished, this, &SondeHub::handleReply);
+}
+
+SondeHub::~SondeHub()
+{
+ disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SondeHub::handleReply);
+ delete m_networkManager;
+}
+
+SondeHub* SondeHub::create()
+{
+ return new SondeHub();
+}
+
+void SondeHub::upload(
+ const QString uploaderCallsign,
+ QDateTime timeReceived,
+ RS41Frame *frame,
+ const RS41Subframe *subframe,
+ float uploaderLat,
+ float uploaderLon,
+ float uploaderAlt
+ )
+{
+ // Check we have required data
+ if (!frame->m_statusValid || !frame->m_posValid) {
+ return;
+ }
+
+ QJsonArray uploaderPos {
+ uploaderLat, uploaderLon, uploaderAlt
+ };
+
+ QJsonObject obj {
+ {"software_name", "SDRangel"},
+ {"software_version", qApp->applicationVersion()},
+ {"uploader_callsign", uploaderCallsign},
+ {"time_received", timeReceived.toUTC().toString("yyyy-MM-ddTHH:mm:ss.zzz000Z")},
+ {"manufacturer", "Vaisala"},
+ {"type", "RS41"},
+ {"uploader_position", uploaderPos}
+ };
+
+ if (frame->m_statusValid)
+ {
+ obj.insert("frame", frame->m_frameNumber);
+ obj.insert("serial", frame->m_serial);
+ obj.insert("batt", frame->m_batteryVoltage);
+ }
+
+ if (frame->m_measValid)
+ {
+ // Don't upload uncalibrated measurements, as there can be a significant error
+ if (frame->isTemperatureCalibrated()) {
+ obj.insert("temp", frame->getTemperatureFloat(subframe));
+ }
+ if (frame->isHumidityCalibrated())
+ {
+ float humidity = frame->getHumidityFloat(subframe);
+ if (humidity != 0.0f) {
+ obj.insert("humidity", humidity);
+ }
+ }
+ if (frame->isPressureCalibrated())
+ {
+ float pressure = frame->getPressureFloat(subframe);
+ if (pressure != 0.0f) {
+ obj.insert("pressure", pressure);
+ }
+ }
+ }
+
+ if (frame->m_gpsInfoValid)
+ {
+ obj.insert("datetime", frame->m_gpsDateTime.toUTC().addSecs(18).toString("yyyy-MM-ddTHH:mm:ss.zzz000Z")); // +18 adjusts UTC to GPS time
+ }
+
+ if (frame->m_posValid)
+ {
+ obj.insert("lat", frame->m_latitude);
+ obj.insert("lon", frame->m_longitude);
+ obj.insert("alt", frame->m_height);
+ obj.insert("vel_h", frame->m_speed);
+ obj.insert("vel_v", frame->m_verticalRate);
+ obj.insert("heading", frame->m_heading);
+ obj.insert("sats", frame->m_satellitesUsed);
+ }
+
+ if (!subframe->getFrequencyMHz().isEmpty()) {
+ obj.insert("frequency", std::round(subframe->getFrequencyMHz().toFloat() * 100.0) / 100.0);
+ }
+
+ if (subframe->getType() != "RS41") {
+ obj.insert("subtype", subframe->getType());
+ }
+
+ //qDebug() << obj;
+ QJsonArray payloads {
+ obj
+ };
+
+ QJsonDocument doc(payloads);
+ QByteArray data = doc.toJson();
+
+ QUrl url(QString("https://api.v2.sondehub.org/sondes/telemetry"));
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setHeader(QNetworkRequest::UserAgentHeader, "sdrangel");
+ request.setRawHeader("Date", QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).toLatin1());
+
+ m_networkManager->put(request, data);
+}
+
+void SondeHub::updatePosition(
+ const QString& callsign,
+ float latitude,
+ float longitude,
+ float altitude,
+ const QString& radio,
+ const QString& antenna,
+ const QString& email,
+ bool mobile
+ )
+{
+ QJsonArray position {
+ latitude, longitude, altitude
+ };
+
+ QJsonObject obj {
+ {"software_name", "SDRangel"},
+ {"software_version", qApp->applicationVersion()},
+ {"uploader_callsign", callsign},
+ {"uploader_position", position},
+ {"uploader_radio", radio},
+ {"uploader_antenna", antenna},
+ {"uploader_contact_email", email},
+ {"mobile", mobile}
+ };
+
+ QJsonDocument doc(obj);
+ QByteArray data = doc.toJson();
+
+ QUrl url(QString("https://api.v2.sondehub.org/listeners"));
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setHeader(QNetworkRequest::UserAgentHeader, "sdrangel");
+
+ m_networkManager->put(request, data);
+}
+
+void SondeHub::handleReply(QNetworkReply* reply)
+{
+ if (reply)
+ {
+ if (!reply->error())
+ {
+ QByteArray bytes = reply->readAll();
+ //qDebug() << bytes;
+ }
+ else
+ {
+ qDebug() << "SondeHub::handleReply: error: " << reply->error() << reply->readAll();
+ }
+ reply->deleteLater();
+ }
+ else
+ {
+ qDebug() << "SondeHub::handleReply: reply is null";
+ }
+}
diff --git a/sdrbase/util/sondehub.h b/sdrbase/util/sondehub.h
new file mode 100644
index 000000000..6ad663a6f
--- /dev/null
+++ b/sdrbase/util/sondehub.h
@@ -0,0 +1,74 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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_SONDEHUB_H
+#define INCLUDE_SONDEHUB_H
+
+#include
+#include
+
+#include "export.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class RS41Frame;
+class RS41Subframe;
+
+class SDRBASE_API SondeHub : public QObject
+{
+ Q_OBJECT
+protected:
+ SondeHub();
+
+public:
+
+ static SondeHub* create();
+
+ ~SondeHub();
+
+ void upload(
+ const QString uploaderCallsign,
+ QDateTime timeReceived,
+ RS41Frame *frame,
+ const RS41Subframe *subframe,
+ float uploaderLat,
+ float uploaderLon,
+ float uploaderAlt
+ );
+
+ void updatePosition(
+ const QString& callsign,
+ float latitude,
+ float longitude,
+ float altitude,
+ const QString& radio,
+ const QString& antenna,
+ const QString& email,
+ bool mobile
+ );
+
+
+private slots:
+ void handleReply(QNetworkReply* reply);
+
+private:
+
+ QNetworkAccessManager *m_networkManager;
+
+};
+
+#endif /* INCLUDE_SONDEHUB_H */