1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-22 17:45:48 -05:00

Radiosonde: Support uploading to SondeHub. Improve humidity calculation. Fix a couple of bugs.

This commit is contained in:
srcejon 2024-04-10 01:31:39 +01:00
parent 872bc8f13f
commit a77b6f1b36
16 changed files with 849 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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();
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RadiosondeFeedSettingsDialog</class>
<widget class="QDialog" name="RadiosondeFeedSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>441</width>
<height>211</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>SondeHub Feed Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>SondeHub Feed Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="callsignLabel">
<property name="text">
<string>Callsign</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="callsign">
<property name="toolTip">
<string>Callsign of feeder / uploader</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="emailLabel">
<property name="text">
<string>E-mail</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="email">
<property name="toolTip">
<string>E-mail of feeder / uploader</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="displayPositionLabel">
<property name="text">
<string>Display position</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="displayPosition">
<property name="toolTip">
<string>Check to publically display receiver position on SondeHub map</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="mobileLabel">
<property name="text">
<string>Mobile</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="mobile">
<property name="toolTip">
<string>Check to indicate if receiver is mobile (E.g. chase car)</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="antennaLabel">
<property name="text">
<string>Antenna</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="antenna">
<property name="toolTip">
<string>Description of antenna</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RadiosondeFeedSettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>257</x>
<y>31</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RadiosondeFeedSettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>325</x>
<y>31</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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<int>(&QComboBox::currentIndexChanged), this, &RadiosondeGUI::on_y1_currentIndexChanged);
QObject::connect(ui->y2, qOverload<int>(&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();
}
}

View File

@ -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_

View File

@ -399,6 +399,23 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="feed">
<property name="toolTip">
<string>Enable feeding of received frames to SondeHub. Right click for settings.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/txon.png</normaloff>:/txon.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -419,6 +436,11 @@
</widget>
</widget>
<customwidgets>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>RollupContents</class>
<extends>QWidget</extends>

View File

@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// 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;
}

View File

@ -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 <f4exb06@gmail.com> //
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// 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];

View File

@ -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.
<h2>Interface</h2>
![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)
<h3>Feeding Data to SondeHub</h3>
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.
<h2>Attribution</h2>
* Hot-air-balloon icons created by Freepik - https://www.flaticon.com/free-icons/hot-air-balloon

View File

@ -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

View File

@ -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
{

View File

@ -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;

202
sdrbase/util/sondehub.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "sondehub.h"
#include "util/radiosonde.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
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";
}
}

74
sdrbase/util/sondehub.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_SONDEHUB_H
#define INCLUDE_SONDEHUB_H
#include <QtCore>
#include <QDateTime>
#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 */