diff --git a/CMakeLists.txt b/CMakeLists.txt index 9623bb869..a8dde1192 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -312,7 +312,8 @@ if (BUILD_GUI) QuickWidgets Positioning Location - Charts) + Charts + TextToSpeech) endif() # other requirements diff --git a/doc/img/SatelliteTracker_plugin.png b/doc/img/SatelliteTracker_plugin.png new file mode 100644 index 000000000..77977317f Binary files /dev/null and b/doc/img/SatelliteTracker_plugin.png differ diff --git a/doc/img/SatelliteTracker_plugin_control.png b/doc/img/SatelliteTracker_plugin_control.png new file mode 100644 index 000000000..b849f1bab Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_control.png differ diff --git a/doc/img/SatelliteTracker_plugin_map.png b/doc/img/SatelliteTracker_plugin_map.png new file mode 100644 index 000000000..f67d0951f Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_map.png differ diff --git a/doc/img/SatelliteTracker_plugin_passchart.png b/doc/img/SatelliteTracker_plugin_passchart.png new file mode 100644 index 000000000..92fda83d2 Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_passchart.png differ diff --git a/doc/img/SatelliteTracker_plugin_satdata.png b/doc/img/SatelliteTracker_plugin_satdata.png new file mode 100644 index 000000000..773c1d250 Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_satdata.png differ diff --git a/doc/img/SatelliteTracker_plugin_selection.png b/doc/img/SatelliteTracker_plugin_selection.png new file mode 100644 index 000000000..94e56647d Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_selection.png differ diff --git a/doc/img/SatelliteTracker_plugin_settings.png b/doc/img/SatelliteTracker_plugin_settings.png new file mode 100644 index 000000000..57ae02f9c Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_settings.png differ diff --git a/doc/img/SatelliteTracker_plugin_settingsdialog1.png b/doc/img/SatelliteTracker_plugin_settingsdialog1.png new file mode 100644 index 000000000..160b1e6f6 Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_settingsdialog1.png differ diff --git a/doc/img/SatelliteTracker_plugin_settingsdialog2.png b/doc/img/SatelliteTracker_plugin_settingsdialog2.png new file mode 100644 index 000000000..208d87b51 Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_settingsdialog2.png differ diff --git a/doc/img/SatelliteTracker_plugin_settingsdialog3.png b/doc/img/SatelliteTracker_plugin_settingsdialog3.png new file mode 100644 index 000000000..90cb8ba0f Binary files /dev/null and b/doc/img/SatelliteTracker_plugin_settingsdialog3.png differ diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 716ffd55b..fbe613f39 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -359,6 +359,57 @@ if ((NOT LIBDSDCC_FOUND OR LIBDSDCC_EXTERNAL) AND LIBMBE_FOUND) endif () endif ((NOT LIBDSDCC_FOUND OR LIBDSDCC_EXTERNAL) AND LIBMBE_FOUND) +# For APT demodulator +set(APT_LIBRARIES "${SDRANGEL_BINARY_LIB_DIR}/apt.lib" CACHE INTERNAL "") +ExternalProject_Add(apt + GIT_REPOSITORY https://github.com/srcejon/aptdec.git + GIT_TAG libaptdec + PREFIX "${EXTERNAL_BUILD_LIBRARIES}/apt" + CMAKE_ARGS ${COMMON_CMAKE_ARGS} + BUILD_BYPRODUCTS "${APT_LIBRARIES}" + INSTALL_COMMAND "" + TEST_COMMAND "" + ) +ExternalProject_Get_Property(apt source_dir binary_dir) +set(APT_FOUND ON CACHE INTERNAL "") +set(APT_EXTERNAL ON CACHE INTERNAL "") +set(APT_INCLUDE_DIR "${EXTERNAL_BUILD_LIBRARIES}/apt/src/apt/src" CACHE INTERNAL "") +if (WIN32) + install(FILES "${SDRANGEL_BINARY_BIN_DIR}/apt${CMAKE_SHARED_LIBRARY_SUFFIX}" DESTINATION "${INSTALL_LIB_DIR}") +elseif (APPLE) + set(APT_LIBRARIES "${binary_dir}/libapt${CMAKE_SHARED_LIBRARY_SUFFIX}" CACHE INTERNAL "") + install(DIRECTORY "${binary_dir}/" DESTINATION "${INSTALL_LIB_DIR}" + FILES_MATCHING PATTERN "libapt*${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(MACOS_EXTERNAL_LIBS_FIXUP "${MACOS_EXTERNAL_LIBS_FIXUP};${binary_dir}/") +endif () + +# For Satellite Tracker feature +# No tags for this in github - but doesn't change often +# Fails to build with CMAKE_INTERPROCEDURAL_OPTIMIZATION=ON on Windows +set(SGP4_LIBRARIES "${SDRANGEL_BINARY_LIB_DIR}/sgp4s.lib" CACHE INTERNAL "") +ExternalProject_Add(sgp4 + GIT_REPOSITORY https://github.com/dnwrnr/sgp4.git + PREFIX "${EXTERNAL_BUILD_LIBRARIES}/sgp4" + CMAKE_ARGS ${COMMON_CMAKE_ARGS} + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF + BUILD_BYPRODUCTS "${SGP4_LIBRARIES}" + INSTALL_COMMAND "" + TEST_COMMAND "" + ) +ExternalProject_Get_Property(sgp4 source_dir binary_dir) +set(SGP4_FOUND ON CACHE INTERNAL "") +set(SGP4_EXTERNAL ON CACHE INTERNAL "") +set(SGP4_INCLUDE_DIR "${EXTERNAL_BUILD_LIBRARIES}/sgp4/src/sgp4/libsgp4" CACHE INTERNAL "") +if (WIN32) + install(FILES "${SDRANGEL_BINARY_BIN_DIR}/sgp4s${CMAKE_SHARED_LIBRARY_SUFFIX}" DESTINATION "${INSTALL_LIB_DIR}") +elseif (APPLE) + set(SGP4_LIBRARIES "${binary_dir}/libsgp4s${CMAKE_SHARED_LIBRARY_SUFFIX}" CACHE INTERNAL "") + install(DIRECTORY "${binary_dir}/" DESTINATION "${INSTALL_LIB_DIR}" + FILES_MATCHING PATTERN "libsgp4s*${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(MACOS_EXTERNAL_LIBS_FIXUP "${MACOS_EXTERNAL_LIBS_FIXUP};${binary_dir}/") +endif () + + # requirements needed by many packages on windows if (WIN32) ExternalProject_Add(pthreads4w diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 3bd131099..df3ba354d 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -16,4 +16,7 @@ add_subdirectory(aprs) add_subdirectory(demodanalyzer) add_subdirectory(rigctlserver) add_subdirectory(simpleptt) +if (SGP4_FOUND) + add_subdirectory(satellitetracker) +endif() add_subdirectory(startracker) diff --git a/plugins/feature/satellitetracker/CMakeLists.txt b/plugins/feature/satellitetracker/CMakeLists.txt new file mode 100644 index 000000000..18ba79a44 --- /dev/null +++ b/plugins/feature/satellitetracker/CMakeLists.txt @@ -0,0 +1,83 @@ +project(satellitetracker) + +set(satellitetracker_SOURCES + satellitetracker.cpp + satellitetrackersettings.cpp + satellitetrackerplugin.cpp + satellitetrackerworker.cpp + satellitetrackerwebapiadapter.cpp + satellitetrackersgp4.cpp +) + +set(satellitetracker_HEADERS + satellitetracker.h + satellitetrackersettings.h + satellitetrackerplugin.h + satellitetrackerreport.h + satellitetrackerworker.h + satellitetrackerwebapiadapter.h + satellitetrackersgp4.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${SGP4_INCLUDE_DIR} +) + +if(NOT SERVER_MODE) + set(satellitetracker_SOURCES + ${satellitetracker_SOURCES} + satellitetrackergui.cpp + satellitetrackergui.ui + satellitetrackersettingsdialog.cpp + satellitetrackersettingsdialog.ui + satellitetracker.qrc + satelliteselectiondialog.cpp + satelliteselectiondialog.ui + satelliteradiocontroldialog.cpp + satelliteradiocontroldialog.ui + satellitedevicesettingsgui.cpp + ) + set(satellitetracker_HEADERS + ${satellitetracker_HEADERS} + satellitetrackergui.h + satellitetrackersettingsdialog.h + satelliteselectiondialog.h + satelliteradiocontroldialog.h + satellitedevicesettingsgui.h + ) + + set(TARGET_NAME featuresatellitetracker) + set(TARGET_LIB Qt5::Widgets Qt5::Positioning Qt5::Charts Qt5::TextToSpeech) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featuresatellitetrackersrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${satellitetracker_SOURCES} +) + +if(SGP4_EXTERNAL) + add_dependencies(${TARGET_NAME} sgp4) +endif() + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + ${SGP4_LIBRARIES} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + # Run deployqt for Charts and TextToSpeech etc + include(DeployQt) + windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/aprs) +endif() diff --git a/plugins/feature/satellitetracker/readme.md b/plugins/feature/satellitetracker/readme.md new file mode 100644 index 000000000..46e5528a5 --- /dev/null +++ b/plugins/feature/satellitetracker/readme.md @@ -0,0 +1,197 @@ +

Satellite Tracker Feature Plugin

+ +

Introduction

+ +The Satellite Tracker feature plugin can be used to: + +* Track satellites, pointing antennas at them via SDRangel's Rotator Controller Features +* Control SDRangel by loading presets, starting/stopping acqusition and setting center frequenies on AOS (Acquisition of Signal) for each satellite +* Adjust channels' input frequency offset to account for Doppler shift +* Display polar and elevation/azimuth vs time plots for satellite passes +* Display the overhead position of satellites on the Map Feature, along with the ground track of the satellites +* Display a variety of information about the satellite + +![Satellite Tracker feature plugin GUI](../../../doc/img/SatelliteTracker_plugin.png) + +

Interface

+ +![Satellite Tracker settings](../../../doc/img/SatelliteTracker_plugin_settings.png) + +

1: Start/Stop plugin

+ +This button starts or stops the satellite tracking. The plugin will only calculate satellite positions or adjust for Doppler when started. + +

2: Find satellite on map

+ +Pressing this button centres the Map Feature (if open) on the target satellite. + +

3: Automatically select target on AOS

+ +When checked, the target satellite will be automatically changed on any selected satellite's AOS, if it is a higher priority than the current target or if the current target satellite is not visible. +Priority is determined by the order the satellites appear in the Satellite Selection dialog. + +

4: Update satellite data

+ +When clicked, the TLE (two line element) files selected in the Settings dialog are downloaded as well as the latest SatNogs satellite database, containing details of satellite's transmitter and receiver frequencies. While downloading, this button will appear green. + +Satellite positions can only be predicted with limited accuracy, so without the TLEs need to be updated frequently for accurate positioning. This could be daily, weekly or monthly depending upon the individual satellite. This downloads around 1MB of data. + +

5: Show SDRangel Control dialog

+ +Pressing this button displays the SDRangel Control dialog. + +![SDRangel Control dialog](../../../doc/img/SatellitTracker_plugin_control.png) + +This dialog determines the actions the Satellite Tracker will take when AOS or LOS occurs for a satellite. First, select a satellite from the dropdown box. Information about the satellites transmit and receive modes should appear in the field at the bottom of the dialog, if available in the SatNogs database. + +To perform an action on an SDRangel device set on AOS or LOS, press the + button. This will add a row in the table, allowing you to select: + +* The device set that will be controlled. This will list all currently open device sets. You can also type the name of a new device set. +* The preset to load on AOS. This allows preset device settings (E.g. centre frequency) and demodulators to be opened when the satellite becomes visible. +* Which channels Doppler correction should be applied to. The list of channels is taken from the selected preset. Check a channel to enable Doppler correct for that channel. The Doppler correction is applied to the channel's input frequency offset. +* Whether to start acquisition (i.e. start the DDR device) on AOS. +* Whether to start acquisition on LOS. +* Whether and file sinks in the preset should be started on AOS and stopped on LOS. This allows the baseband signal received from the satellite to be recorded to a file. +* Whether to override the centre frequency in the preset. This allows a single preset to be used with multiple satellites. +* A command or script to execute on AOS. +* A command or script to execute on LOS. + +Multiple rows can be added, to allow independent control of multiple device sets. To remove a row, select the row by clicking the row number, then press the - button. + +

6: Show Satellite Selection dialog

+ +Pressing this button displays the Satellite Selection dialog. + +![Satellite Selection dialog](../../../doc/img/SatellitTracker_plugin_selection.png) + +On the left hand side are a list of all available satellites, as determined by the TLE files that have been downloaded. (If none are visible, ensure the TLEs tab of the Satellite Settings dialog (8) contains at least https://db.satnogs.org/api/tle/ and then press the Update satellite data (4) button) + +The list of satellites that the Satellite Tracker will track is on the right hand side. + +To move satellites from side to side, either double click them, or select them and press the left or right arrows in the middle. + +The Satellites to track list is ordered in priority for the auto target feature (3). The change the order, select a satellite in the list and press the up or down arrows to the right. + +Satellite information at the bottom of the dialog comes from the SatNogs database: https://db.satnogs.org/ + +

7: Set latitude and longitude from My Position

+ +When clicked, it sets the latitude, longitude and height fields to the values from SDRangel's My Position preferences. + +

8: Show Settings dialog

+ +Pressing this button displays the Settings dialog. + +![Satellite tracker settings dialog](../../../doc/img/SatellitTracker_plugin_settingsdialog1.png) + +On the Settings tab, you can set: + +* Height above sea level in metres of the anntenna. +* The prediciton period in days. This limits the maximum number of days ahead for which satellite passes are predicted until. +* The minimum elevation in degrees from the antenna location, which a satellite much reach in order for AOS to be indicated. +* The minimum elevation in degrees from the antenna location, which a satellite much reach in order for a pass to be indicated. +* A time window for which passes must start and end between, to be displayed or acted upon. For example, for day time passes, you could set "must start after" to 8:00 and "must end before" to 18:00. For night time passes, set "must start after" to 20:00 and "must end before" to 6:00. +* The maximum azimuth angle in degrees supported by your rotator. 450 degree support is beneficial for passes that pass through 360/0 degrees, to avoid the rotator having to do a complete rotation mid pass. +* The maximum elevation angle in degrees supported by your rotator. 180 degree support is beneficial for passes that pass through 360/0 degrees, to avoid the rotator having to do a complete rotation mid pass. +* A speech warning to be given on AOS. ${name} will be subsitited with the name of the satellite, ${duration} the pass duration and ${elevation} the maximum elevation of the pass. +* A speech warning to be given on LOS. ${name} will be subsitited with the name of the satellite. +* A command/script to be executed on AOS. This applies to all satellites. It is also possible to set a per-satellite command in the SDRangel Control dialog. +* A command/script to be executed on LOS. This applies to all satellites. It is also possible to set a per-satellite command in the SDRangel Control dialog. +* The Doppler correction period in seconds, which controls how frequently Doppler correction is applied. Which channels have Doppler correction applied is set on a per-channel basis in the SDRangel Control dialog. + +![Satellite tracker settings dialog](../../../doc/img/SatellitTracker_plugin_settingsdialog2.png) + +On the TLEs tab, you can provide a list of URL from which satellite Two Line Element files can be downloaded from. +TLE files contain the orbital parameters for a satellite and are required in order to be able to calculate a satellites position. + +![Satellite tracker settings dialog](../../../doc/img/SatellitTracker_plugin_settingsdialog3.png) + +On the display tab, you can set: + +* The update period in seconds, which controls how frequently satellite positions are calculated. +* The default frequency in MHz that is used for calculating Doppler and free space path loss in the Satellite Data table. +* The units used to display azimuth and elevation to the target satellite. This can be in degrees, minutes and seconds or decimal degrees. +* The number of points used for ground tracks on the map. More points result in smoother tracks, but require more processing. +* Whether times are display in the local time zone or UTC. +* Whether to draw the satellites on the map. + +

9: Latitude/h3> + +Specifies the latitude in decimal degrees (North positive) of the antenna location. + +

10: Longitude

+ +Specifies the longitude in decimal degrees (East positive) of the antenna location. + +

11: Time

+ +Select the date and time at which the position of the satellite should be calculated. Select either Now, for the current time, or Custom to manually enter a date and time. + +

12: Target

+ +Select the target satellite. The target satellite is the source of data for the Time to AOS, Azimuth and Elevation fields. The azimuth and elevation of the target satellite is sent to the Rotator Controller features. + +

13: Time to AOS

+ +This field displays the time to AOS (Acquisition of Signal) for the target satellite. It is displayed in hours, minutes and seconds, unless the satellite is currently visible, in which case it will display "Now". + +

14: Azimuth

+ +Displays the calculated azimuth (angle in degrees, clockwise from North) to the target satellite. + +

15: Elevation

+ +Displays the calculated elevation (angle in degrees - 0 to horizon and 90 to zenith) to the target satellite. + +

Pass Charts

+ +Pass charts can be plotted showing the azimuth and elevation of the target satellite from AOS to LOS. This can be in polar of Cartesian form: + +![Satellite tracker settings dialog](../../../doc/img/SatellitTracker_plugin_passchart.png) + +The arrows next to the chart combobox, allow the pass number to be selected. Pass 0 is the next pass, with higher numbered passes occuring later in time. +The amount of passes is determined by the prediction period, which can be set in the Settings dialog. + +

Satellite Data

+ +The satellite data table displays calculated data about the selected satellites. + +![Satellite data table](../../../doc/img/SatellitTracker_plugin_satdata.png) + +The table contains: + +* The satellite name. +* The azimuth in degrees to the satellite from the antenna location. +* The elevation in degress to the satellite from the antenna location. +* The time of the next AOS. If time is some days in the future, the number of days will be displayed as +days. E.g. +1 for tomorrow. +* The time of the next LOS. +* The maximum elevation in degrees that the satellite will be from the antenna location in the next pass. +* Whether the satellite will be heading South to North (up addow) or North to South (down arrow) in the next pass. +* The altitude of the satellite in kilometres. +* The range to the satellite from the antenna location in kilometers. +* The range range (i.e. speed the satellite is moving away from the antenna location) in kilometres per second. +* The Doppler shift due to the satellite's motion that would be observed on a signal at the default frequency (which can be set in the Settings dialog). +* The free space path loss to the satellite, at the default frequency. +* The one-way propagation delay to the satellite from the antenna location in milliseconds. +* The NORAD catalog identifier for the satellite. + +Rows can be ordered by left clicking column headers. +Columns can be hidden by right clicking on the header and unchecking them. + +

Map

+ +The Satellite Tracker feature can send the overhead position of the satellite to the Map, along with a ground track. + +When using the Find feature in the Map GUI, you can search by the name of the satellite. + +![SatelliteTracker map](../../../doc/img/SatelliteTracker_map.png) + +

Attribution

+ +sgp4 library by Daniel Warner https://github.com/dnwrnr + +SatNogs satellite database https://db.satnogs.org/ + +Satellite two-line elements (TLEs) are from Celestrak https://celestrak.com/ + +Icons are by Freepik from Flaticon https://www.flaticon.com/ diff --git a/plugins/feature/satellitetracker/satellitedevicesettingsgui.cpp b/plugins/feature/satellitetracker/satellitedevicesettingsgui.cpp new file mode 100644 index 000000000..09ca2c546 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitedevicesettingsgui.cpp @@ -0,0 +1,290 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 +#include +#include + +#include "satellitedevicesettingsgui.h" +#include "device/deviceset.h" +#include "settings/mainsettings.h" +#include "maincore.h" +#include "util/messagequeue.h" +#include "plugin/pluginmanager.h" +#include "plugin/pluginapi.h" + +SatelliteDeviceSettingsGUI::SatelliteDeviceSettingsGUI(SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings, + QTableWidget *table) +{ + m_devSettings = devSettings; + + // Device set + m_deviceSetWidget = new QComboBox(); + m_deviceSetWidget->setEditable(true); + m_deviceSetWidget->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_DEVICESET)->toolTip()); + m_deviceSetItem = new QWidget(); + layout(m_deviceSetItem, m_deviceSetWidget); + addDeviceSets(); + int devSetIdx = m_deviceSetWidget->findText(devSettings->m_deviceSet); + if (devSetIdx != -1) + m_deviceSetWidget->setCurrentIndex(devSetIdx); + else + { + m_deviceSetWidget->addItem(devSettings->m_deviceSet); + m_deviceSetWidget->setCurrentIndex(m_deviceSetWidget->count() - 1); + } + + // Preset + m_presetWidget = new QComboBox(); + m_presetWidget->setEditable(false); + m_presetWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_presetWidget->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_PRESET)->toolTip()); + m_presetItem = new QWidget(); + layout(m_presetItem, m_presetWidget); + addPresets(devSettings->m_deviceSet); + + const MainSettings& mainSettings = MainCore::instance()->getSettings(); + if (!devSettings->m_deviceSet.isEmpty()) + { + int count = mainSettings.getPresetCount(); + int idx = 0; + for (int i = 0; i < count; i++) + { + const Preset *preset = mainSettings.getPreset(i); + if ( ((preset->isSourcePreset() && (devSettings->m_deviceSet[0] == "R"))) + || ((preset->isSinkPreset() && (devSettings->m_deviceSet[0] == "T"))) + || ((preset->isMIMOPreset() && (devSettings->m_deviceSet[0] == "M")))) + { + if ( (devSettings->m_presetGroup == preset->getGroup()) + && (devSettings->m_presetFrequency == preset->getCenterFrequency()) + && (devSettings->m_presetDescription == preset->getDescription())) + { + m_presetWidget->setCurrentIndex(idx); + break; + } + idx++; + } + } + } + + // Doppler + m_dopplerWidget = new QComboBox(); + m_dopplerWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_dopplerWidget->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_DOPPLER)->toolTip()); + m_dopplerItem = new QWidget(); + layout(m_dopplerItem, m_dopplerWidget); + m_dopplerWidget->setModel(&m_dopplerModel); + addChannels(); + + for (int i = 0; i < devSettings->m_doppler.size(); i++) + m_dopplerItems[devSettings->m_doppler[i]]->setData(Qt::Checked, Qt::CheckStateRole); + + // Start on AOS + m_startOnAOSWidget = new QCheckBox(); + m_startOnAOSWidget->setChecked(devSettings->m_startOnAOS); + m_startOnAOSWidget->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_START)->toolTip()); + m_startOnAOSItem = new QWidget(); + layout(m_startOnAOSItem, m_startOnAOSWidget); + + // Stop on AOS + m_stopOnLOSWidget = new QCheckBox(); + m_stopOnLOSWidget->setChecked(devSettings->m_stopOnLOS); + m_stopOnLOSWidget->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_STOP)->toolTip()); + m_stopOnLOSItem = new QWidget(); + layout(m_stopOnLOSItem, m_stopOnLOSWidget); + + // Start file sink + m_startStopFileSinkWidget = new QCheckBox(); + m_startStopFileSinkWidget->setChecked(devSettings->m_startStopFileSink); + m_startStopFileSinkWidget->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_START_FILE_SINK)->toolTip()); + m_startStopFileSinkItem = new QWidget(); + layout(m_startStopFileSinkItem, m_startStopFileSinkWidget); + + // Frequency override + m_frequencyItem = new QTableWidgetItem(); + m_frequencyItem->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_FREQUENCY)->toolTip()); + if (devSettings->m_frequency != 0) + m_frequencyItem->setData(Qt::DisplayRole, QString("%1").arg(devSettings->m_frequency/1000000.0, 0, 'f', 3, QLatin1Char(' '))); + + // AOS command + m_aosCommandItem = new QTableWidgetItem(); + m_aosCommandItem->setText(devSettings->m_aosCommand); + m_aosCommandItem->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_AOS_COMMAND)->toolTip()); + + // LOS command + m_losCommandItem = new QTableWidgetItem(); + m_losCommandItem->setText(devSettings->m_losCommand); + m_losCommandItem->setToolTip(table->horizontalHeaderItem(SAT_DEVICE_COL_LOS_COMMAND)->toolTip()); + + int row = table->rowCount(); + table->setRowCount(row + 1); + table->setCellWidget(row, SAT_DEVICE_COL_DEVICESET, m_deviceSetItem); + table->setCellWidget(row, SAT_DEVICE_COL_PRESET, m_presetItem); + table->setCellWidget(row, SAT_DEVICE_COL_DOPPLER, m_dopplerItem); + table->setCellWidget(row, SAT_DEVICE_COL_START, m_startOnAOSItem); + table->setCellWidget(row, SAT_DEVICE_COL_STOP, m_stopOnLOSItem); + table->setCellWidget(row, SAT_DEVICE_COL_START_FILE_SINK, m_startStopFileSinkItem); + table->setItem(row, SAT_DEVICE_COL_FREQUENCY, m_frequencyItem); + table->setItem(row, SAT_DEVICE_COL_AOS_COMMAND, m_aosCommandItem); + table->setItem(row, SAT_DEVICE_COL_LOS_COMMAND, m_losCommandItem); + table->resizeColumnsToContents(); + + connect(m_deviceSetWidget, SIGNAL(currentTextChanged(const QString &)), this, SLOT(on_m_deviceSetWidget_currentTextChanged(const QString &))); + connect(m_presetWidget, SIGNAL(currentIndexChanged(int)), this, SLOT(on_m_presetWidget_currentIndexChanged(int))); +} + +void SatelliteDeviceSettingsGUI::layout(QWidget *parent, QWidget *child) +{ + QHBoxLayout* pLayout = new QHBoxLayout(parent); + pLayout->addWidget(child); + pLayout->setAlignment(Qt::AlignCenter); + pLayout->setContentsMargins(0, 0, 0, 0); + parent->setLayout(pLayout); +} + +// Add available devicesets to the combobox +void SatelliteDeviceSettingsGUI::addDeviceSets() +{ + MainCore *mainCore = MainCore::instance(); + std::vector& deviceSets = mainCore->getDeviceSets(); + std::vector::const_iterator it = deviceSets.begin(); + for (unsigned int deviceIndex = 0; it != deviceSets.end(); ++it, deviceIndex++) + { + DSPDeviceSourceEngine *deviceSourceEngine = (*it)->m_deviceSourceEngine; + DSPDeviceSinkEngine *deviceSinkEngine = (*it)->m_deviceSinkEngine; + + if (deviceSourceEngine) { + m_deviceSetWidget->addItem(QString("R%1").arg(deviceIndex), deviceIndex); + } else if (deviceSinkEngine) { + m_deviceSetWidget->addItem(QString("T%1").arg(deviceIndex), deviceIndex); + } + } +} + +// Add all available presets for a deviceset to the combobox +void SatelliteDeviceSettingsGUI::addPresets(const QString& deviceSet) +{ + m_presetWidget->clear(); + const MainSettings& mainSettings = MainCore::instance()->getSettings(); + int count = mainSettings.getPresetCount(); + m_currentPresets = deviceSet[0]; + for (int i = 0; i < count; i++) + { + const Preset *preset = mainSettings.getPreset(i); + if ( ((preset->isSourcePreset() && (m_currentPresets == "R"))) + || ((preset->isSinkPreset() && (m_currentPresets == "T"))) + || ((preset->isMIMOPreset() && (m_currentPresets == "M")))) + { + m_presetWidget->addItem(QString("%1: %2 - %3") + .arg(preset->getGroup()) + .arg(preset->getCenterFrequency()/1000000.0, 0, 'f', 3) + .arg(preset->getDescription())); + } + } +} + +const Preset* SatelliteDeviceSettingsGUI::getSelectedPreset() +{ + int listIdx = m_presetWidget->currentIndex(); + const MainSettings& mainSettings = MainCore::instance()->getSettings(); + int count = mainSettings.getPresetCount(); + int presetIdx = 0; + for (int i = 0; i < count; i++) + { + const Preset *preset = mainSettings.getPreset(i); + if ( ((preset->isSourcePreset() && (m_currentPresets == "R"))) + || ((preset->isSinkPreset() && (m_currentPresets == "T"))) + || ((preset->isMIMOPreset() && (m_currentPresets == "M")))) + { + if (listIdx == presetIdx) + return preset; + presetIdx++; + } + } + return nullptr; +} + +// Add checkable list of channels from a preset to the combobox +void SatelliteDeviceSettingsGUI::addChannels() +{ + m_dopplerModel.clear(); + m_dopplerItems.clear(); + const PluginManager *pluginManager = MainCore::instance()->getPluginManager(); + const Preset* preset = getSelectedPreset(); + if (preset != nullptr) + { + int channels = preset->getChannelCount(); + for (int i = 0; i < channels; i++) + { + const Preset::ChannelConfig& channelConfig = preset->getChannelConfig(i); + + QStandardItem *item = new QStandardItem(); + item->setText(pluginManager->uriToId(channelConfig.m_channelIdURI)); + item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + item->setData(Qt::Unchecked, Qt::CheckStateRole); + m_dopplerModel.appendRow(item); + m_dopplerItems.append(item); + } + } +} + +// Update preset list, to match type of deviceset entered +void SatelliteDeviceSettingsGUI::on_m_deviceSetWidget_currentTextChanged(const QString &text) +{ + if (!text.isEmpty()) + { + if (text[0] != m_currentPresets) + addPresets(text[0]); + } +} + +// Update doppler combo, to correspond to selected preset +void SatelliteDeviceSettingsGUI::on_m_presetWidget_currentIndexChanged(int index) +{ + addChannels(); +} + +// Update devSettings with current GUI values +void SatelliteDeviceSettingsGUI::accept() +{ + m_devSettings->m_deviceSet = m_deviceSetWidget->currentText(); + const Preset* preset = getSelectedPreset(); + if (preset != nullptr) + { + m_devSettings->m_presetGroup = preset->getGroup(); + m_devSettings->m_presetFrequency = preset->getCenterFrequency(); + m_devSettings->m_presetDescription = preset->getDescription(); + } + else + { + m_devSettings->m_presetGroup = ""; + m_devSettings->m_presetFrequency = 0; + m_devSettings->m_presetDescription = ""; + } + m_devSettings->m_doppler.clear(); + for (int i = 0; i < m_dopplerItems.size(); i++) + { + if (m_dopplerItems[i]->checkState() == Qt::Checked) + m_devSettings->m_doppler.append(i); + } + m_devSettings->m_startOnAOS = m_startOnAOSWidget->isChecked(); + m_devSettings->m_stopOnLOS = m_stopOnLOSWidget->isChecked(); + m_devSettings->m_startStopFileSink = m_startStopFileSinkWidget->isChecked(); + m_devSettings->m_frequency = (quint64)(m_frequencyItem->data(Qt::DisplayRole).toDouble() * 1000000.0); + m_devSettings->m_aosCommand = m_aosCommandItem->text(); + m_devSettings->m_losCommand = m_losCommandItem->text(); +} diff --git a/plugins/feature/satellitetracker/satellitedevicesettingsgui.h b/plugins/feature/satellitetracker/satellitedevicesettingsgui.h new file mode 100644 index 000000000..9e43c603b --- /dev/null +++ b/plugins/feature/satellitetracker/satellitedevicesettingsgui.h @@ -0,0 +1,94 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATELLITEDEVICESETTINGSGUI_H +#define INCLUDE_FEATURE_SATELLITEDEVICESETTINGSGUI_H + +#include +#include +#include +#include +#include +#include + +#include "settings/preset.h" +#include "satellitetrackersettings.h" + +class SatelliteRadioControlDialog; + +class SatelliteDeviceSettingsGUI : public QObject +{ + Q_OBJECT + +public: + + SatelliteDeviceSettingsGUI(SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings, + QTableWidget *table); + void accept(); + +protected: + + void layout(QWidget *parent, QWidget *child); + void addDeviceSets(); + void addPresets(const QString& deviceSet); + void addChannels(); + const Preset *getSelectedPreset(); + +private slots: + + void on_m_deviceSetWidget_currentTextChanged(const QString &text); + void on_m_presetWidget_currentIndexChanged(int index); + +protected: + + friend SatelliteRadioControlDialog; + QWidget *m_deviceSetItem; + QComboBox *m_deviceSetWidget; + QWidget *m_presetItem; + QComboBox *m_presetWidget; + QWidget *m_dopplerItem; + QComboBox *m_dopplerWidget; + QWidget *m_startOnAOSItem; + QCheckBox *m_startOnAOSWidget; + QWidget *m_stopOnLOSItem; + QCheckBox *m_stopOnLOSWidget; + QWidget *m_startStopFileSinkItem; + QCheckBox *m_startStopFileSinkWidget; + QTableWidgetItem *m_frequencyItem; + QTableWidgetItem *m_aosCommandItem; + QTableWidgetItem *m_losCommandItem; + QChar m_currentPresets; + + QStandardItemModel m_dopplerModel; + QList m_dopplerItems; + + SatelliteTrackerSettings::SatelliteDeviceSettings *m_devSettings; + + enum SatDeviceCol { + SAT_DEVICE_COL_DEVICESET, + SAT_DEVICE_COL_PRESET, + SAT_DEVICE_COL_DOPPLER, + SAT_DEVICE_COL_START, + SAT_DEVICE_COL_STOP, + SAT_DEVICE_COL_START_FILE_SINK, + SAT_DEVICE_COL_FREQUENCY, + SAT_DEVICE_COL_AOS_COMMAND, + SAT_DEVICE_COL_LOS_COMMAND + }; +}; + +#endif // INCLUDE_FEATURE_SATELLITEDEVICESETTINGSGUI_H diff --git a/plugins/feature/satellitetracker/satelliteradiocontroldialog.cpp b/plugins/feature/satellitetracker/satelliteradiocontroldialog.cpp new file mode 100644 index 000000000..319ac766a --- /dev/null +++ b/plugins/feature/satellitetracker/satelliteradiocontroldialog.cpp @@ -0,0 +1,147 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 +#include +#include +#include + +#include "device/deviceset.h" +#include "settings/mainsettings.h" +#include "settings/preset.h" +#include "maincore.h" +#include "util/messagequeue.h" +#include "satelliteradiocontroldialog.h" + +SatelliteRadioControlDialog::SatelliteRadioControlDialog(SatelliteTrackerSettings *settings, + const QHash& satellites, + QWidget* parent) : + QDialog(parent), + m_settings(settings), + m_satellites(satellites), + ui(new Ui::SatelliteRadioControlDialog) +{ + ui->setupUi(this); + + // Must resize before setting m_deviceSettings + resizeTable(); + + m_deviceSettings = m_settings->m_deviceSettings; + + for (int i = 0; i < settings->m_satellites.size(); i++) + ui->satelliteSelect->addItem(settings->m_satellites[i]); +} + +SatelliteRadioControlDialog::~SatelliteRadioControlDialog() +{ + delete ui; +} + +void SatelliteRadioControlDialog::accept() +{ + for (int i = 0; i < m_devSettingsGUIs.size(); i++) + m_devSettingsGUIs[i]->accept(); + QDialog::accept(); + m_settings->m_deviceSettings = m_deviceSettings; +} + +void SatelliteRadioControlDialog::resizeTable() +{ + on_add_clicked(); + ui->table->resizeColumnsToContents(); + ui->table->selectRow(0); + on_remove_clicked(); + ui->table->selectRow(-1); +} + +void SatelliteRadioControlDialog::on_add_clicked() +{ + QString name = ui->satelliteSelect->currentText(); + if (!name.isEmpty()) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = new SatelliteTrackerSettings::SatelliteDeviceSettings(); + SatelliteDeviceSettingsGUI *devSettingsGUI = new SatelliteDeviceSettingsGUI(devSettings, ui->table); + + m_devSettingsGUIs.append(devSettingsGUI); + QList *devSettingsList = m_deviceSettings.value(name); + devSettingsList->append(devSettings); + } +} + +// Remove selected row +void SatelliteRadioControlDialog::on_remove_clicked() +{ + // Selection mode is single, so only a single row should be returned + QModelIndexList indexList = ui->table->selectionModel()->selectedRows(); + if (!indexList.isEmpty()) + { + int row = indexList.at(0).row(); + ui->table->removeRow(row); + delete m_devSettingsGUIs.takeAt(row); + + QString name = ui->satelliteSelect->currentText(); + QList *devSettingsList = m_deviceSettings.value(name); + delete devSettingsList->takeAt(row); + } +} + +void SatelliteRadioControlDialog::on_satelliteSelect_currentIndexChanged(int index) +{ + // Save details from current GUI elements + for (int i = 0; i < m_devSettingsGUIs.size(); i++) + m_devSettingsGUIs[i]->accept(); + // Clear GUI + ui->table->setRowCount(0); + qDeleteAll(m_devSettingsGUIs); + m_devSettingsGUIs.clear(); + + // Create settings list for newly selected satellite, if one doesn't already exist + QString name = ui->satelliteSelect->currentText(); + if (!m_deviceSettings.contains(name)) + m_deviceSettings.insert(name, new QList()); + + // Add existing settings to GUI + QList *devSettingsList = m_deviceSettings.value(name); + for (int i = 0; i < devSettingsList->size(); i++) + { + SatelliteDeviceSettingsGUI *devSettingsGUI = new SatelliteDeviceSettingsGUI(devSettingsList->at(i), ui->table); + m_devSettingsGUIs.append(devSettingsGUI); + } + + // Display modes for the satellite, to help user select appropriate presets + SatNogsSatellite *sat = m_satellites[name]; + QStringList info; + for (int i = 0; i < sat->m_transmitters.size(); i++) + { + if (sat->m_transmitters[i]->m_status != "invalid") + { + QStringList mode; + mode.append(" "); + mode.append(sat->m_transmitters[i]->m_description); + if (sat->m_transmitters[i]->m_downlinkHigh > 0) + mode.append(QString("D: %1").arg(SatNogsTransmitter::getFrequencyRangeText(sat->m_transmitters[i]->m_downlinkLow, sat->m_transmitters[i]->m_downlinkHigh))); + else if (sat->m_transmitters[i]->m_downlinkLow > 0) + mode.append(QString("D: %1").arg(SatNogsTransmitter::getFrequencyText(sat->m_transmitters[i]->m_downlinkLow))); + if (sat->m_transmitters[i]->m_uplinkHigh > 0) + mode.append(QString("U: %1").arg(SatNogsTransmitter::getFrequencyRangeText(sat->m_transmitters[i]->m_uplinkLow, sat->m_transmitters[i]->m_uplinkHigh))); + else if (sat->m_transmitters[i]->m_uplinkLow > 0) + mode.append(QString("U: %1").arg(SatNogsTransmitter::getFrequencyText(sat->m_transmitters[i]->m_uplinkLow))); + info.append(mode.join(" ")); + } + } + ui->satelliteModes->setText(info.join("\n")); +} diff --git a/plugins/feature/satellitetracker/satelliteradiocontroldialog.h b/plugins/feature/satellitetracker/satelliteradiocontroldialog.h new file mode 100644 index 000000000..260e981ee --- /dev/null +++ b/plugins/feature/satellitetracker/satelliteradiocontroldialog.h @@ -0,0 +1,53 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATELLITERADIOCONTROLDIALOG_H +#define INCLUDE_FEATURE_SATELLITERADIOCONTROLDIALOG_H + +#include + +#include "ui_SatelliteRadioControlDialog.h" +#include "satellitetrackersettings.h" +#include "satellitedevicesettingsgui.h" +#include "satnogs.h" + +class SatelliteRadioControlDialog : public QDialog { + Q_OBJECT + +public: + explicit SatelliteRadioControlDialog(SatelliteTrackerSettings* settings, const QHash& satellites, QWidget* parent = 0); + ~SatelliteRadioControlDialog(); + + SatelliteTrackerSettings *m_settings; + +private: + void resizeTable(); + +private slots: + void accept(); + void on_add_clicked(); + void on_remove_clicked(); + void on_satelliteSelect_currentIndexChanged(int index); + +private: + const QHash& m_satellites; + QHash *> m_deviceSettings; // Device settings per sateillite + QList m_devSettingsGUIs; // For selected satellite + Ui::SatelliteRadioControlDialog* ui; +}; + +#endif // INCLUDE_FEATURE_SATELLITERADIOCONTROLDIALOG_H diff --git a/plugins/feature/satellitetracker/satelliteradiocontroldialog.ui b/plugins/feature/satellitetracker/satelliteradiocontroldialog.ui new file mode 100644 index 000000000..6a4b87daa --- /dev/null +++ b/plugins/feature/satellitetracker/satelliteradiocontroldialog.ui @@ -0,0 +1,259 @@ + + + SatelliteRadioControlDialog + + + + 0 + 0 + 955 + 400 + + + + + Liberation Sans + 9 + + + + Qt::PreventContextMenu + + + Satellite Radio Control + + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + Device set + + + Device set to control + + + + + Preset to load on AOS + + + Preset to load to device set + + + + + Doppler correction + + + Channel numbers that will have Doppler correction applied + + + + + Start on AOS + + + Start acquisition on AOS + + + + + Stop on LOS + + + Stop acquisition on LOS + + + + + Start/stop file sinks + + + Start file sinks recording on AOS and stop recording on LOS + + + + + Override frequency (MHz) + + + Override the center frequency in the preset with a value specified here in MHz. +This allows a single preset to be shared between different satellites that differ only in frequency. + + + + + AOS command + + + Command to execute on AOS + + + + + LOS command + + + Command to execute on LOS + + + + + + + + Satellite modes from SatNOGS + + + true + + + + + + + + + Satellite + + + + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Add device set control + + + + + + + + + + + Remove device set control + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Satellite modes + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + satelliteSelect + table + add + remove + satelliteModes + + + + + + + buttonBox + accepted() + SatelliteRadioControlDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SatelliteRadioControlDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/satellitetracker/satelliteselectiondialog.cpp b/plugins/feature/satellitetracker/satelliteselectiondialog.cpp new file mode 100644 index 000000000..eb61a8f59 --- /dev/null +++ b/plugins/feature/satellitetracker/satelliteselectiondialog.cpp @@ -0,0 +1,281 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 +#include +#include +#include + +#include +#include + +#include "satelliteselectiondialog.h" +#include "util/units.h" + +SatelliteSelectionDialog::SatelliteSelectionDialog(SatelliteTrackerSettings *settings, + const QHash& satellites, + QWidget* parent) : + QDialog(parent), + m_settings(settings), + m_satellites(satellites), + m_satInfo(nullptr), + ui(new Ui::SatelliteSelectionDialog) +{ + ui->setupUi(this); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + + QHashIterator itr(satellites); + while (itr.hasNext()) + { + itr.next(); + QString name = itr.key(); + SatNogsSatellite *sat = itr.value(); + // Don't display decayed satellites, or those without TLEs + if ((sat->m_status == "alive") || (sat->m_status == "")) + { + if (sat->m_tle != nullptr) + { + if (settings->m_satellites.indexOf(name) == -1) + ui->availableSats->addItem(name); + } + else + qDebug() << "SatelliteSelectionDialog::SatelliteSelectionDialog: No TLE for " << name; + } + } + for (int i = 0; i < settings->m_satellites.size(); i++) + ui->selectedSats->addItem(settings->m_satellites[i]); +} + +SatelliteSelectionDialog::~SatelliteSelectionDialog() +{ + delete ui; +} + +void SatelliteSelectionDialog::accept() +{ + m_settings->m_satellites.clear(); + for (int i = 0; i < ui->selectedSats->count(); i++) + m_settings->m_satellites.append(ui->selectedSats->item(i)->text()); + QDialog::accept(); +} + +void SatelliteSelectionDialog::on_find_textChanged(const QString &text) +{ + QString textTrimmed = text.trimmed(); + QList items = ui->availableSats->findItems(textTrimmed, Qt::MatchContains); + if (items.size() > 0) + ui->availableSats->setCurrentItem(items[0]); + else + { + // Try alternative names + QHashIterator itr(m_satellites); + while (itr.hasNext()) + { + itr.next(); + SatNogsSatellite *sat = itr.value(); + if (sat->m_names.indexOf(textTrimmed) != -1) + { + QList items = ui->availableSats->findItems(sat->m_name, Qt::MatchExactly); + if (items.size() > 0) + ui->availableSats->setCurrentItem(items[0]); + break; + } + } + } +} + +void SatelliteSelectionDialog::on_addSat_clicked() +{ + QList items = ui->availableSats->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + ui->selectedSats->addItem(items[i]->text()); + delete items[i]; + } +} + +void SatelliteSelectionDialog::on_removeSat_clicked() +{ + QList items = ui->selectedSats->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + ui->availableSats->addItem(items[i]->text()); + delete items[i]; + } +} + +void SatelliteSelectionDialog::on_moveUp_clicked() +{ + QList items = ui->selectedSats->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = ui->selectedSats->row(items[i]); + if (row > 0) + { + QListWidgetItem *item = ui->selectedSats->takeItem(row); + ui->selectedSats->insertItem(row - 1, item); + ui->selectedSats->setCurrentItem(item); + } + } +} + +void SatelliteSelectionDialog::on_moveDown_clicked() +{ + QList items = ui->selectedSats->selectedItems(); + for (int i = items.size() - 1; i >= 0; i--) + { + int row = ui->selectedSats->row(items[i]); + if (row < ui->selectedSats->count() - 1) + { + QListWidgetItem *item = ui->selectedSats->takeItem(row); + ui->selectedSats->insertItem(row + 1, item); + ui->selectedSats->setCurrentItem(item); + } + } +} + +void SatelliteSelectionDialog::on_availableSats_itemDoubleClicked(QListWidgetItem *item) +{ + ui->selectedSats->addItem(item->text()); + delete item; +} + +void SatelliteSelectionDialog::on_selectedSats_itemDoubleClicked(QListWidgetItem *item) +{ + ui->availableSats->addItem(item->text()); + delete item; +} + +void SatelliteSelectionDialog::on_availableSats_itemSelectionChanged() +{ + QList items = ui->availableSats->selectedItems(); + if (items.size() > 0) + { + ui->selectedSats->selectionModel()->clear(); + displaySatInfo(items[0]->text()); + } +} + +void SatelliteSelectionDialog::on_selectedSats_itemSelectionChanged() +{ + QList items = ui->selectedSats->selectedItems(); + if (items.size() > 0) + { + ui->availableSats->selectionModel()->clear(); + displaySatInfo(items[0]->text()); + } +} + +// Display information about the satellite from the SatNOGS database +void SatelliteSelectionDialog::displaySatInfo(const QString& name) +{ + SatNogsSatellite *sat = m_satellites[name]; + m_satInfo = sat; + QStringList info; + info.append(QString("Name: %1").arg(sat->m_name)); + if (sat->m_names.size() > 0) + info.append(QString("Alternative names: %1").arg(sat->m_names.join(" "))); + info.append(QString("NORAD ID: %1").arg(sat->m_noradCatId)); + if (sat->m_launched.isValid()) + info.append(QString("Launched: %1").arg(sat->m_launched.toString())); + if (sat->m_deployed.isValid()) + info.append(QString("Deployed: %1").arg(sat->m_deployed.toString())); + if (sat->m_decayed.isValid()) + info.append(QString("Decayed: %1").arg(sat->m_decayed.toString())); + ui->openSatelliteWebsite->setEnabled(!sat->m_website.isEmpty()); + if (!sat->m_operator.isEmpty() && sat->m_operator != "None") + info.append(QString("Operator: %1").arg(sat->m_operator)); + if (!sat->m_countries.isEmpty()) + info.append(QString("Countries: %1").arg(sat->m_countries)); + if (sat->m_transmitters.size() > 0) + info.append("Modes:"); + for (int i = 0; i < sat->m_transmitters.size(); i++) + { + if (sat->m_transmitters[i]->m_status != "invalid") + { + QStringList mode; + mode.append(" "); + mode.append(sat->m_transmitters[i]->m_description); + if (sat->m_transmitters[i]->m_downlinkHigh > 0) + mode.append(QString("D: %1").arg(SatNogsTransmitter::getFrequencyRangeText(sat->m_transmitters[i]->m_downlinkLow, sat->m_transmitters[i]->m_downlinkHigh))); + else if (sat->m_transmitters[i]->m_downlinkLow > 0) + mode.append(QString("D: %1").arg(SatNogsTransmitter::getFrequencyText(sat->m_transmitters[i]->m_downlinkLow))); + if (sat->m_transmitters[i]->m_uplinkHigh > 0) + mode.append(QString("U: %1").arg(SatNogsTransmitter::getFrequencyRangeText(sat->m_transmitters[i]->m_uplinkLow, sat->m_transmitters[i]->m_uplinkHigh))); + else if (sat->m_transmitters[i]->m_uplinkLow > 0) + mode.append(QString("U: %1").arg(SatNogsTransmitter::getFrequencyText(sat->m_transmitters[i]->m_uplinkLow))); + info.append(mode.join(" ")); + } + } + if (sat->m_tle != nullptr) + { + info.append("Orbit:"); + Tle tle = Tle(sat->m_tle->m_tle0.toStdString(), + sat->m_tle->m_tle1.toStdString(), + sat->m_tle->m_tle2.toStdString()); + OrbitalElements ele(tle); + info.append(QString(" Period: %1 mins").arg(ele.Period())); + info.append(QString(" Inclination: %1%2").arg(Units::radiansToDegrees(ele.Inclination())).arg(QChar(0xb0))); + info.append(QString(" Eccentricity: %1").arg(ele.Eccentricity())); + } + + ui->satInfo->setText(info.join("\n")); + if (!sat->m_image.isEmpty()) + m_networkManager->get(QNetworkRequest(QUrl(sat->m_image))); + else + ui->satImage->setPixmap(QPixmap()); +} + +// Open the Satellite's webpage +void SatelliteSelectionDialog::on_openSatelliteWebsite_clicked() +{ + if ((m_satInfo != nullptr) && (!m_satInfo->m_website.isEmpty())) + QDesktopServices::openUrl(QUrl(m_satInfo->m_website)); +} + +// Open SatNOGS observations website for the selected satellite +void SatelliteSelectionDialog::on_openSatNogsObservations_clicked() +{ + if (m_satInfo != nullptr) + QDesktopServices::openUrl(QUrl(QString("https://network.satnogs.org/observations/?norad=%1").arg(m_satInfo->m_noradCatId))); +} + +void SatelliteSelectionDialog::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "SatelliteSelectionDialog::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + // Read image data and display it + QByteArray imageData = reply->readAll(); + QPixmap pixmap; + if (pixmap.loadFromData(imageData)) + ui->satImage->setPixmap(pixmap.scaled( ui->satImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + else + qDebug() << "SatelliteSelectionDialog::networkManagerFinished: Failed to load pixmap from image data"; + } + + reply->deleteLater(); +} diff --git a/plugins/feature/satellitetracker/satelliteselectiondialog.h b/plugins/feature/satellitetracker/satelliteselectiondialog.h new file mode 100644 index 000000000..a6973b615 --- /dev/null +++ b/plugins/feature/satellitetracker/satelliteselectiondialog.h @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATELLITESELECTIONDIALOG_H +#define INCLUDE_SATELLITESELECTIONDIALOG_H + +#include +#include + +#include "ui_satelliteselectiondialog.h" +#include "satellitetrackersettings.h" +#include "satnogs.h" + +class QNetworkAccessManager; +class QNetworkReply; + +class SatelliteSelectionDialog : public QDialog { + Q_OBJECT + +public: + explicit SatelliteSelectionDialog(SatelliteTrackerSettings* settings, const QHash& satellites, QWidget* parent = 0); + ~SatelliteSelectionDialog(); + + SatelliteTrackerSettings *m_settings; + +private: + void displaySatInfo(const QString& name); + +private slots: + void accept(); + void on_find_textChanged(const QString &text); + void on_addSat_clicked(); + void on_removeSat_clicked(); + void on_moveUp_clicked(); + void on_moveDown_clicked(); + void on_availableSats_itemDoubleClicked(QListWidgetItem *item); + void on_selectedSats_itemDoubleClicked(QListWidgetItem *item); + void on_availableSats_itemSelectionChanged(); + void on_selectedSats_itemSelectionChanged(); + void on_openSatelliteWebsite_clicked(); + void on_openSatNogsObservations_clicked(); + void networkManagerFinished(QNetworkReply *reply); + +private: + QNetworkAccessManager *m_networkManager; + const QHash& m_satellites; + SatNogsSatellite *m_satInfo; + Ui::SatelliteSelectionDialog* ui; +}; + +#endif // INCLUDE_SATELLITESELECTIONDIALOG_H diff --git a/plugins/feature/satellitetracker/satelliteselectiondialog.ui b/plugins/feature/satellitetracker/satelliteselectiondialog.ui new file mode 100644 index 000000000..13f997d3a --- /dev/null +++ b/plugins/feature/satellitetracker/satelliteselectiondialog.ui @@ -0,0 +1,357 @@ + + + SatelliteSelectionDialog + + + + 0 + 0 + 696 + 561 + + + + + Liberation Sans + 9 + + + + Select satellites to track + + + + + + + 0 + 0 + + + + Satellite selection + + + + + + + + Available satellites + + + + + + + Satellites to track + + + + + + + + + + + List of available satellites. Double click or press right arrow to move to selected list. + + + QAbstractItemView::ExtendedSelection + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Add satellite to selected list + + + + + + + :/arrow_left.png:/arrow_left.png + + + + + + + Remove satellite from selected list + + + + + + + :/arrow_right.png:/arrow_right.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + List of selected satellites. Double click or press left arrow to move to available list. Order according to priority for automatic selection on AOS. + + + QAbstractItemView::ExtendedSelection + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Move satellite up in list + + + + + + + :/arrow_up.png:/arrow_up.png + + + + + + + Move satellite down in list + + + + + + + :/arrow_down.png:/arrow_down.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Find + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + Enter name of satellite to find in the available satellites list + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Satellite information + + + + + + Information from SatNOGS about the selected satellite + + + true + + + + + + + + + + 0 + 0 + + + + Image of satellite + + + + + + false + + + + + + + + + Display website for the satellite + + + Satellite website + + + + + + + Display SatNOGS observations of the satellite + + + SatNOGS observations + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + SatelliteSelectionDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SatelliteSelectionDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/satellitetracker/satellitetracker.cpp b/plugins/feature/satellitetracker/satellitetracker.cpp new file mode 100644 index 000000000..a57a576ea --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetracker.cpp @@ -0,0 +1,883 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureReport.h" +#include "SWGFeatureActions.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" +#include "util/httpdownloadmanager.h" + +#include "satellitetrackerworker.h" +#include "satellitetracker.h" + +MESSAGE_CLASS_DEFINITION(SatelliteTracker::MsgConfigureSatelliteTracker, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTracker::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTracker::MsgUpdateSatData, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTracker::MsgSatData, Message) + +const char* const SatelliteTracker::m_featureIdURI = "sdrangel.feature.satellitetracker"; +const char* const SatelliteTracker::m_featureId = "SatelliteTracker"; + +SatelliteTracker::SatelliteTracker(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_updatingSatData(false), + m_tleIndex(0), + m_firstUpdateSatData(true) +{ + qDebug("SatelliteTracker::SatelliteTracker: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_worker = new SatelliteTrackerWorker(this, webAPIAdapterInterface); + m_state = StIdle; + m_errorMessage = "SatelliteTracker error"; + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &SatelliteTracker::downloadFinished); + + if (!readSatData()) + updateSatData(); +} + +SatelliteTracker::~SatelliteTracker() +{ + if (m_worker->isRunning()) { + stop(); + } + + delete m_worker; +} + +void SatelliteTracker::start() +{ + qDebug("SatelliteTracker::start"); + + m_worker->reset(); + m_worker->setMessageQueueToFeature(getInputMessageQueue()); + m_worker->setMessageQueueToGUI(getMessageQueueToGUI()); + bool ok = m_worker->startWork(); + m_state = ok ? StRunning : StError; + m_thread.start(); + + m_worker->getInputMessageQueue()->push(SatelliteTrackerWorker::MsgConfigureSatelliteTrackerWorker::create(m_settings, true)); + m_worker->getInputMessageQueue()->push(MsgSatData::create(m_satellites)); +} + +void SatelliteTracker::stop() +{ + qDebug("SatelliteTracker::stop"); + m_worker->stopWork(); + m_state = StIdle; + m_thread.quit(); + m_thread.wait(); +} + +bool SatelliteTracker::handleMessage(const Message& cmd) +{ + if (MsgConfigureSatelliteTracker::match(cmd)) + { + MsgConfigureSatelliteTracker& cfg = (MsgConfigureSatelliteTracker&) cmd; + qDebug() << "SatelliteTracker::handleMessage: MsgConfigureSatelliteTracker"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + qDebug() << "SatelliteTracker::handleMessage: MsgStartStop: start:" << cfg.getStartStop(); + + if (cfg.getStartStop()) { + start(); + } else { + stop(); + } + + return true; + } + else if (MsgUpdateSatData::match(cmd)) + { + // When the GUI first opens, it will make an initial request to update the sats + // In the first instance, just return the data we've read + if (m_firstUpdateSatData && (m_satellites.size() > 0)) + { + if (m_guiMessageQueue) + m_guiMessageQueue->push(MsgSatData::create(m_satellites)); + m_firstUpdateSatData = false; + } + else + updateSatData(); + return true; + } + else + { + return false; + } +} + +QByteArray SatelliteTracker::serialize() const +{ + return m_settings.serialize(); +} + +bool SatelliteTracker::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureSatelliteTracker *msg = MsgConfigureSatelliteTracker::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureSatelliteTracker *msg = MsgConfigureSatelliteTracker::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void SatelliteTracker::applySettings(const SatelliteTrackerSettings& settings, bool force) +{ + bool tlesChanged = false; + + qDebug() << "SatelliteTracker::applySettings:" + << " m_latitude: " << settings.m_latitude + << " m_longitude: " << settings.m_longitude + << " m_heightAboveSeaLevel: " << settings.m_heightAboveSeaLevel + << " m_target: " << settings.m_target + << " m_satellites: " << settings.m_satellites + << " m_tles: " << settings.m_tles + << " m_dateTime: " << settings.m_dateTime + << " m_minAOSElevation: " << settings.m_minAOSElevation + << " m_minPassElevation: " << settings.m_minPassElevation + << " m_azElUnits: " << settings.m_azElUnits + << " m_groundTrackPoints: " << settings.m_groundTrackPoints + << " m_dateFormat: " << settings.m_dateFormat + << " m_utc: " << settings.m_utc + << " m_updatePeriod: " << settings.m_updatePeriod + << " m_dopplerPeriod: " << settings.m_dopplerPeriod + << " m_defaultFrequency: " << settings.m_defaultFrequency + << " m_drawOnMap: " << settings.m_drawOnMap + << " m_autoTarget: " << settings.m_autoTarget + << " m_aosSpeech: " << settings.m_aosSpeech + << " m_losSpeech: " << settings.m_losSpeech + << " m_aosCommand: " << settings.m_aosCommand + << " m_losCommand: " << settings.m_losCommand + << " m_predictionPeriod: " << settings.m_predictionPeriod + << " m_passStartTime: " << settings.m_passStartTime + << " m_passFinishTime: " << settings.m_passFinishTime + << " m_deviceSettings: " << settings.m_deviceSettings + << " m_title: " << settings.m_title + << " m_rgbColor: " << settings.m_rgbColor + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex + << " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((m_settings.m_latitude != settings.m_latitude) || force) { + reverseAPIKeys.append("latitude"); + } + if ((m_settings.m_longitude != settings.m_longitude) || force) { + reverseAPIKeys.append("longitude"); + } + if ((m_settings.m_heightAboveSeaLevel != settings.m_heightAboveSeaLevel) || force) { + reverseAPIKeys.append("heightAboveSeaLevel"); + } + if ((m_settings.m_target != settings.m_target) || force) { + reverseAPIKeys.append("target"); + } + if ((m_settings.m_satellites != settings.m_satellites) || force) { + reverseAPIKeys.append("satellites"); + } + if ((m_settings.m_tles != settings.m_tles) || force) { + tlesChanged = true; + reverseAPIKeys.append("tles"); + } + if ((m_settings.m_dateTime != settings.m_dateTime) || force) { + reverseAPIKeys.append("dateTime"); + } + if ((m_settings.m_minAOSElevation != settings.m_minAOSElevation) || force) { + reverseAPIKeys.append("minAOSElevation"); + } + if ((m_settings.m_minPassElevation != settings.m_minPassElevation) || force) { + reverseAPIKeys.append("minPassElevation"); + } + if ((m_settings.m_azElUnits != settings.m_azElUnits) || force) { + reverseAPIKeys.append("azElUnits"); + } + if ((m_settings.m_groundTrackPoints != settings.m_groundTrackPoints) || force) { + reverseAPIKeys.append("groundTrackPoints"); + } + if ((m_settings.m_dateFormat != settings.m_dateFormat) || force) { + reverseAPIKeys.append("dateFormat"); + } + if ((m_settings.m_utc != settings.m_utc) || force) { + reverseAPIKeys.append("utc"); + } + if ((m_settings.m_updatePeriod != settings.m_updatePeriod) || force) { + reverseAPIKeys.append("updatePeriod"); + } + if ((m_settings.m_dopplerPeriod != settings.m_dopplerPeriod) || force) { + reverseAPIKeys.append("dopplerPeriod"); + } + if ((m_settings.m_defaultFrequency != settings.m_defaultFrequency) || force) { + reverseAPIKeys.append("defaultFrequency"); + } + if ((m_settings.m_drawOnMap != settings.m_drawOnMap) || force) { + reverseAPIKeys.append("drawOnMap"); + } + if ((m_settings.m_autoTarget != settings.m_autoTarget) || force) { + reverseAPIKeys.append("autoTarget"); + } + if ((m_settings.m_aosSpeech != settings.m_aosSpeech) || force) { + reverseAPIKeys.append("aosSpeech"); + } + if ((m_settings.m_losSpeech != settings.m_losSpeech) || force) { + reverseAPIKeys.append("losSpeech"); + } + if ((m_settings.m_aosCommand != settings.m_aosCommand) || force) { + reverseAPIKeys.append("aosCommand"); + } + if ((m_settings.m_losCommand != settings.m_losCommand) || force) { + reverseAPIKeys.append("losCommand"); + } + if ((m_settings.m_predictionPeriod != settings.m_predictionPeriod) || force) { + reverseAPIKeys.append("predictionPeriod"); + } + if ((m_settings.m_passStartTime != settings.m_passStartTime) || force) { + reverseAPIKeys.append("passStartTime"); + } + if ((m_settings.m_passFinishTime != settings.m_passFinishTime) || force) { + reverseAPIKeys.append("passFinishTime"); + } + if ((m_settings.m_deviceSettings != settings.m_deviceSettings) || force) { + reverseAPIKeys.append("deviceSettings"); + } + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + SatelliteTrackerWorker::MsgConfigureSatelliteTrackerWorker *msg = SatelliteTrackerWorker::MsgConfigureSatelliteTrackerWorker::create( + settings, force + ); + m_worker->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) || + (m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; + + if (tlesChanged) + { + // Do we already have the TLE files, or do we need to download them? + bool existing = true; + for (int i = 0; i < m_settings.m_tles.size(); i++) + { + QFile tlesFile(tleURLToFilename(m_settings.m_tles[i])); + if (!tlesFile.exists()) + { + existing = false; + break; + } + } + if (existing) + readSatData(); + else + updateSatData(); + } +} + +int SatelliteTracker::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + getFeatureStateStr(*response.getState()); + MsgStartStop *msg = MsgStartStop::create(run); + getInputMessageQueue()->push(msg); + return 202; +} + +int SatelliteTracker::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSatelliteTrackerSettings(new SWGSDRangel::SWGSatelliteTrackerSettings()); + response.getSatelliteTrackerSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int SatelliteTracker::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + SatelliteTrackerSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureSatelliteTracker *msg = MsgConfigureSatelliteTracker::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("SatelliteTracker::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureSatelliteTracker *msgToGUI = MsgConfigureSatelliteTracker::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + return 200; +} + +static QList *convertStringListToPtrs(QStringList listIn) +{ + QList *listOut = new QList(); + + for (int i = 0; i < listIn.size(); i++) + listOut->append(new QString(listIn[i])); + + return listOut; +} + +static QStringList convertPtrsToStringList(QList *listIn) +{ + QStringList listOut; + + for (int i = 0; i < listIn->size(); i++) + listOut.append(*listIn->at(i)); + + return listOut; +} + +void SatelliteTracker::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const SatelliteTrackerSettings& settings) +{ + response.getSatelliteTrackerSettings()->setLatitude(settings.m_latitude); + response.getSatelliteTrackerSettings()->setLongitude(settings.m_longitude); + response.getSatelliteTrackerSettings()->setHeightAboveSeaLevel(settings.m_heightAboveSeaLevel); + response.getSatelliteTrackerSettings()->setTarget(new QString(settings.m_target)); + response.getSatelliteTrackerSettings()->setSatellites(convertStringListToPtrs(settings.m_satellites)); + response.getSatelliteTrackerSettings()->setTles(convertStringListToPtrs(settings.m_tles)); + response.getSatelliteTrackerSettings()->setDateTime(new QString(settings.m_dateTime)); + response.getSatelliteTrackerSettings()->setMinAosElevation(settings.m_minAOSElevation); + response.getSatelliteTrackerSettings()->setMinPassElevation(settings.m_minPassElevation); + response.getSatelliteTrackerSettings()->setAzElUnits((int)settings.m_azElUnits); + response.getSatelliteTrackerSettings()->setGroundTrackPoints(settings.m_groundTrackPoints); + response.getSatelliteTrackerSettings()->setDateFormat(new QString(settings.m_dateFormat)); + response.getSatelliteTrackerSettings()->setUtc(settings.m_utc); + response.getSatelliteTrackerSettings()->setUpdatePeriod(settings.m_updatePeriod); + response.getSatelliteTrackerSettings()->setDopplerPeriod(settings.m_dopplerPeriod); + response.getSatelliteTrackerSettings()->setDefaultFrequency(settings.m_defaultFrequency); + response.getSatelliteTrackerSettings()->setDrawOnMap(settings.m_drawOnMap); + response.getSatelliteTrackerSettings()->setAutoTarget(settings.m_autoTarget); + response.getSatelliteTrackerSettings()->setAosSpeech(new QString(settings.m_aosSpeech)); + response.getSatelliteTrackerSettings()->setLosSpeech(new QString(settings.m_losSpeech)); + response.getSatelliteTrackerSettings()->setAosCommand(new QString(settings.m_aosCommand)); + response.getSatelliteTrackerSettings()->setLosCommand(new QString(settings.m_losCommand)); + response.getSatelliteTrackerSettings()->setPredictionPeriod(settings.m_predictionPeriod); + response.getSatelliteTrackerSettings()->setPassStartTime(new QString(settings.m_passStartTime.toString())); + response.getSatelliteTrackerSettings()->setPassFinishTime(new QString(settings.m_passFinishTime.toString())); + + if (response.getSatelliteTrackerSettings()->getTitle()) { + *response.getSatelliteTrackerSettings()->getTitle() = settings.m_title; + } else { + response.getSatelliteTrackerSettings()->setTitle(new QString(settings.m_title)); + } + + response.getSatelliteTrackerSettings()->setRgbColor(settings.m_rgbColor); + response.getSatelliteTrackerSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getSatelliteTrackerSettings()->getReverseApiAddress()) { + *response.getSatelliteTrackerSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getSatelliteTrackerSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getSatelliteTrackerSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getSatelliteTrackerSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex); + response.getSatelliteTrackerSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex); +} + +void SatelliteTracker::webapiUpdateFeatureSettings( + SatelliteTrackerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("latitude")) { + settings.m_latitude = response.getSatelliteTrackerSettings()->getLatitude(); + } + if (featureSettingsKeys.contains("longitude")) { + settings.m_longitude = response.getSatelliteTrackerSettings()->getLongitude(); + } + if (featureSettingsKeys.contains("heightAboveSeaLevel")) { + settings.m_heightAboveSeaLevel = response.getSatelliteTrackerSettings()->getHeightAboveSeaLevel(); + } + if (featureSettingsKeys.contains("target")) { + settings.m_target = *response.getSatelliteTrackerSettings()->getTarget(); + } + if (featureSettingsKeys.contains("satellites")) { + settings.m_satellites = convertPtrsToStringList(response.getSatelliteTrackerSettings()->getSatellites()); + } + if (featureSettingsKeys.contains("tles")) { + settings.m_tles = convertPtrsToStringList(response.getSatelliteTrackerSettings()->getTles()); + } + if (featureSettingsKeys.contains("dateTime")) { + settings.m_dateTime = *response.getSatelliteTrackerSettings()->getDateTime(); + } + if (featureSettingsKeys.contains("minAOSElevation")) { + settings.m_minAOSElevation = response.getSatelliteTrackerSettings()->getMinAosElevation(); + } + if (featureSettingsKeys.contains("minPassElevation")) { + settings.m_minPassElevation = response.getSatelliteTrackerSettings()->getMinPassElevation(); + } + if (featureSettingsKeys.contains("azElUnits")) { + settings.m_azElUnits = (SatelliteTrackerSettings::AzElUnits)response.getSatelliteTrackerSettings()->getAzElUnits(); + } + if (featureSettingsKeys.contains("groundTrackPoints")) { + settings.m_groundTrackPoints = response.getSatelliteTrackerSettings()->getGroundTrackPoints(); + } + if (featureSettingsKeys.contains("dateFormat")) { + settings.m_dateFormat = *response.getSatelliteTrackerSettings()->getDateFormat(); + } + if (featureSettingsKeys.contains("utc")) { + settings.m_utc = response.getSatelliteTrackerSettings()->getUtc() != 0; + } + if (featureSettingsKeys.contains("updatePeriod")) { + settings.m_updatePeriod = response.getSatelliteTrackerSettings()->getUpdatePeriod(); + } + if (featureSettingsKeys.contains("dopplerPeriod")) { + settings.m_dopplerPeriod = response.getSatelliteTrackerSettings()->getDopplerPeriod(); + } + if (featureSettingsKeys.contains("defaultFrequency")) { + settings.m_defaultFrequency = response.getSatelliteTrackerSettings()->getDefaultFrequency(); + } + if (featureSettingsKeys.contains("drawOnMap")) { + settings.m_drawOnMap = response.getSatelliteTrackerSettings()->getDrawOnMap() != 0; + } + if (featureSettingsKeys.contains("autoTarget")) { + settings.m_autoTarget = response.getSatelliteTrackerSettings()->getAutoTarget() != 0; + } + if (featureSettingsKeys.contains("aosSpeech")) { + settings.m_aosSpeech = *response.getSatelliteTrackerSettings()->getAosSpeech(); + } + if (featureSettingsKeys.contains("losSpeech")) { + settings.m_losSpeech = *response.getSatelliteTrackerSettings()->getLosSpeech(); + } + if (featureSettingsKeys.contains("aosCommand")) { + settings.m_aosCommand = *response.getSatelliteTrackerSettings()->getAosCommand(); + } + if (featureSettingsKeys.contains("losCommand")) { + settings.m_losCommand = *response.getSatelliteTrackerSettings()->getLosCommand(); + } + if (featureSettingsKeys.contains("predictionPeriod")) { + settings.m_predictionPeriod = response.getSatelliteTrackerSettings()->getPredictionPeriod(); + } + if (featureSettingsKeys.contains("passStartTime")) { + settings.m_passStartTime = QTime::fromString(*response.getSatelliteTrackerSettings()->getPassStartTime()); + } + if (featureSettingsKeys.contains("passFinishTime")) { + settings.m_passFinishTime = QTime::fromString(*response.getSatelliteTrackerSettings()->getPassFinishTime()); + } + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getSatelliteTrackerSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getSatelliteTrackerSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getSatelliteTrackerSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getSatelliteTrackerSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getSatelliteTrackerSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getSatelliteTrackerSettings()->getReverseApiDeviceIndex(); + } + if (featureSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIFeatureIndex = response.getSatelliteTrackerSettings()->getReverseApiChannelIndex(); + } +} + +void SatelliteTracker::webapiReverseSendSettings(QList& featureSettingsKeys, const SatelliteTrackerSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("SatelliteTracker")); + swgFeatureSettings->setSatelliteTrackerSettings(new SWGSDRangel::SWGSatelliteTrackerSettings()); + SWGSDRangel::SWGSatelliteTrackerSettings *swgSatelliteTrackerSettings = swgFeatureSettings->getSatelliteTrackerSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("latitude") || force) { + swgSatelliteTrackerSettings->setLatitude(settings.m_latitude); + } + if (featureSettingsKeys.contains("longitude") || force) { + swgSatelliteTrackerSettings->setLongitude(settings.m_longitude); + } + if (featureSettingsKeys.contains("heightAboveSeaLevel") || force) { + swgSatelliteTrackerSettings->setHeightAboveSeaLevel(settings.m_heightAboveSeaLevel); + } + if (featureSettingsKeys.contains("target") || force) { + swgSatelliteTrackerSettings->setTarget(new QString(settings.m_target)); + } + if (featureSettingsKeys.contains("satellites") || force) { + swgSatelliteTrackerSettings->setSatellites(convertStringListToPtrs(settings.m_satellites)); + } + if (featureSettingsKeys.contains("tles") || force) { + swgSatelliteTrackerSettings->setTles(convertStringListToPtrs(settings.m_satellites)); + } + if (featureSettingsKeys.contains("dateTime") || force) { + swgSatelliteTrackerSettings->setDateTime(new QString(settings.m_dateTime)); + } + if (featureSettingsKeys.contains("minAOSElevation") || force) { + swgSatelliteTrackerSettings->setMinAosElevation(settings.m_minAOSElevation); + } + if (featureSettingsKeys.contains("minPassElevation") || force) { + swgSatelliteTrackerSettings->setMinPassElevation(settings.m_minPassElevation); + } + if (featureSettingsKeys.contains("azElUnits") || force) { + swgSatelliteTrackerSettings->setAzElUnits((int)settings.m_azElUnits); + } + if (featureSettingsKeys.contains("groundTrackPoints") || force) { + swgSatelliteTrackerSettings->setGroundTrackPoints(settings.m_groundTrackPoints); + } + if (featureSettingsKeys.contains("dateFormat") || force) { + swgSatelliteTrackerSettings->setDateFormat(new QString(settings.m_dateFormat)); + } + if (featureSettingsKeys.contains("utc") || force) { + swgSatelliteTrackerSettings->setUtc(settings.m_utc); + } + if (featureSettingsKeys.contains("updatePeriod") || force) { + swgSatelliteTrackerSettings->setUpdatePeriod(settings.m_updatePeriod); + } + if (featureSettingsKeys.contains("dopplerPeriod") || force) { + swgSatelliteTrackerSettings->setDopplerPeriod(settings.m_dopplerPeriod); + } + if (featureSettingsKeys.contains("defaultFrequency") || force) { + swgSatelliteTrackerSettings->setDefaultFrequency(settings.m_defaultFrequency); + } + if (featureSettingsKeys.contains("drawOnMap") || force) { + swgSatelliteTrackerSettings->setDrawOnMap(settings.m_drawOnMap); + } + if (featureSettingsKeys.contains("aosSpeech") || force) { + swgSatelliteTrackerSettings->setAosSpeech(new QString(settings.m_aosSpeech)); + } + if (featureSettingsKeys.contains("losSpeech") || force) { + swgSatelliteTrackerSettings->setLosSpeech(new QString(settings.m_losSpeech)); + } + if (featureSettingsKeys.contains("aosCommand") || force) { + swgSatelliteTrackerSettings->setAosCommand(new QString(settings.m_aosCommand)); + } + if (featureSettingsKeys.contains("losCommand") || force) { + swgSatelliteTrackerSettings->setLosCommand(new QString(settings.m_losCommand)); + } + if (featureSettingsKeys.contains("predictionPeriod") || force) { + swgSatelliteTrackerSettings->setPredictionPeriod(settings.m_predictionPeriod); + } + if (featureSettingsKeys.contains("passStartTime") || force) { + swgSatelliteTrackerSettings->setPassStartTime(new QString(settings.m_passStartTime.toString())); + } + if (featureSettingsKeys.contains("passFinishTime") || force) { + swgSatelliteTrackerSettings->setPassFinishTime(new QString(settings.m_passFinishTime.toString())); + } + if (featureSettingsKeys.contains("title") || force) { + swgSatelliteTrackerSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgSatelliteTrackerSettings->setRgbColor(settings.m_rgbColor); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIFeatureSetIndex) + .arg(settings.m_reverseAPIFeatureIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgFeatureSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgFeatureSettings; +} + +void SatelliteTracker::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "SatelliteTracker::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("SatelliteTracker::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +QString SatelliteTracker::satNogsSatellitesFilename() +{ + return HttpDownloadManager::downloadDir() + "/satnogs_satellites.json"; +} + +QString SatelliteTracker::satNogsTransmittersFilename() +{ + return HttpDownloadManager::downloadDir() + "/satnogs_transmitters.json"; +} + +QString SatelliteTracker::satNogsTLEFilename() +{ + return HttpDownloadManager::downloadDir() + "/satnogs_tle.json"; +} + +QString SatelliteTracker::tleURLToFilename(const QString& string) +{ + if (string == "https://db.satnogs.org/api/tle/") + return satNogsTLEFilename(); + QUrl url(string); + return HttpDownloadManager::downloadDir() + "/tle_" + url.fileName(); +} + +void SatelliteTracker::downloadFinished(const QString& filename, bool success) +{ + if (success) + { + if (filename == satNogsSatellitesFilename()) + { + m_dlm.download(QUrl("https://db.satnogs.org/api/transmitters/"), satNogsTransmittersFilename()); + } + else if (filename == satNogsTransmittersFilename()) + { + m_tleIndex = 0; + if (m_settings.m_tles.size() > 0) + m_dlm.download(QUrl(m_settings.m_tles[0]), tleURLToFilename(m_settings.m_tles[0])); + else + qWarning() << "Satellite Tracker: No TLEs"; + } + else if ((m_tleIndex < m_settings.m_tles.size()) && (filename == tleURLToFilename(m_settings.m_tles[m_tleIndex]))) + { + m_tleIndex++; + if (m_tleIndex < m_settings.m_tles.size()) + m_dlm.download(QUrl(m_settings.m_tles[m_tleIndex]), tleURLToFilename(m_settings.m_tles[m_tleIndex])); + else + { + readSatData(); + m_updatingSatData = false; + } + } + else + qDebug() << "SatelliteTracker::downloadFinished: Unexpected filename: " << filename; + } + else + m_updatingSatData = false; +} + +bool SatelliteTracker::readSatData() +{ + QFile satsFile(satNogsSatellitesFilename()); + if (satsFile.open(QIODevice::ReadOnly)) + { + if (parseSatellites(satsFile.readAll())) + { + QFile transmittersFile(satNogsTransmittersFilename()); + if (transmittersFile.open(QIODevice::ReadOnly)) + { + if (parseTransmitters(transmittersFile.readAll())) + { + for (int i = 0; i < m_settings.m_tles.size(); i++) + { + QFile tlesFile(tleURLToFilename(m_settings.m_tles[i])); + if (tlesFile.open(QIODevice::ReadOnly)) + { + bool ok; + if (tlesFile.fileName() == satNogsTLEFilename()) + { + ok = parseSatNogsTLEs(tlesFile.readAll()); + } + else + ok = parseTxtTLEs(tlesFile.readAll()); + if (!ok) + qDebug() << "SatelliteTracker::readSatData - failed to parse: " << tlesFile.fileName(); + } + else + qDebug() << "SatelliteTracker::readSatData - failed to open: " << tlesFile.fileName(); + } + + qDebug() << "SatelliteTracker::readSatData - read " << m_satellites.size() << " satellites"; + + // Send to GUI + if (m_guiMessageQueue) + m_guiMessageQueue->push(MsgSatData::create(m_satellites)); + // Send to worker + m_worker->getInputMessageQueue()->push(MsgSatData::create(m_satellites)); + + return true; + } + } + } + } + qDebug() << "SatelliteTracker::readSatData - Failed to read satellites"; + return false; +} + +bool SatelliteTracker::parseSatellites(const QByteArray& json) +{ + QJsonDocument jsonResponse = QJsonDocument::fromJson(json); + + if (jsonResponse.isArray()) + { + m_satellites = SatNogsSatellite::createHash(jsonResponse.array()); + m_satellitesId.clear(); + + // Create second table, hashed on ID + QHashIterator i(m_satellites); + while (i.hasNext()) + { + i.next(); + SatNogsSatellite *sat = i.value(); + m_satellitesId.insert(sat->m_noradCatId, sat); + } + return true; + } + else + return false; +} + +bool SatelliteTracker::parseTransmitters(const QByteArray& json) +{ + QJsonDocument jsonResponse = QJsonDocument::fromJson(json); + + if (jsonResponse.isArray()) + { + QList transmitters = SatNogsTransmitter::createList(jsonResponse.array()); + + QHashIterator i(m_satellites); + while (i.hasNext()) + { + i.next(); + SatNogsSatellite *sat = i.value(); + sat->addTransmitters(transmitters); + } + return true; + } + else + return false; +} + +bool SatelliteTracker::parseSatNogsTLEs(const QByteArray& json) +{ + QJsonDocument jsonResponse = QJsonDocument::fromJson(json); + + if (jsonResponse.isArray()) + { + QList tles = SatNogsTLE::createList(jsonResponse.array()); + + QHashIterator i(m_satellites); + while (i.hasNext()) + { + i.next(); + SatNogsSatellite *sat = i.value(); + sat->addTLE(tles); + } + return true; + } + else + return false; +} + +bool SatelliteTracker::parseTxtTLEs(const QByteArray& txt) +{ + QList tles = SatNogsTLE::createList(txt); + + QHashIterator i(m_satellites); + while (i.hasNext()) + { + i.next(); + SatNogsSatellite *sat = i.value(); + sat->addTLE(tles); + } + + // Create satellites, that we have TLEs for, but no existing entry + for (int i = 0; i < tles.size(); i++) + { + if (!m_satellitesId.contains(tles[i]->m_noradCatId)) + { + SatNogsSatellite *sat = new SatNogsSatellite(tles[i]); + m_satellites.insert(sat->m_name, sat); + m_satellitesId.insert(sat->m_noradCatId, sat); + } + } + + return true; +} + +void SatelliteTracker::updateSatData() +{ + QMutexLocker mutexLocker(&m_mutex); + + if (m_updatingSatData == false) + { + m_updatingSatData = true; + qDebug() << "SatelliteTracker::updateSatData: requesting satellites"; + m_dlm.download(QUrl("https://db.satnogs.org/api/satellites/"), satNogsSatellitesFilename()); + } + else + qDebug() << "SatelliteTracker::updateSatData: update in progress"; +} diff --git a/plugins/feature/satellitetracker/satellitetracker.h b/plugins/feature/satellitetracker/satellitetracker.h new file mode 100644 index 000000000..53e704959 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetracker.h @@ -0,0 +1,200 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_SATELLITETRACKER_H_ +#define INCLUDE_FEATURE_SATELLITETRACKER_H_ + +#include +#include + +#include "feature/feature.h" +#include "util/message.h" +#include "util/httpdownloadmanager.h" + +#include "satellitetrackersettings.h" +#include "satnogs.h" + +class WebAPIAdapterInterface; +class SatelliteTrackerWorker; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class SatelliteTracker : public Feature +{ + Q_OBJECT +public: + class MsgConfigureSatelliteTracker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const SatelliteTrackerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureSatelliteTracker* create(const SatelliteTrackerSettings& settings, bool force) { + return new MsgConfigureSatelliteTracker(settings, force); + } + + private: + SatelliteTrackerSettings m_settings; + bool m_force; + + MsgConfigureSatelliteTracker(const SatelliteTrackerSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgStartStop : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgStartStop* create(bool startStop) { + return new MsgStartStop(startStop); + } + + protected: + bool m_startStop; + + MsgStartStop(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + class MsgUpdateSatData : public Message { + MESSAGE_CLASS_DECLARATION + + public: + + static MsgUpdateSatData* create() { + return new MsgUpdateSatData(); + } + + private: + + MsgUpdateSatData() : + Message() + { } + }; + + class MsgSatData : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QHash getSatellites() { return m_satellites; } + + static MsgSatData* create(QHash satellites) { + return new MsgSatData(satellites); + } + + private: + QHash m_satellites; + + MsgSatData(QHash satellites) : + Message(), + m_satellites(satellites) + { } + }; + + SatelliteTracker(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~SatelliteTracker(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const SatelliteTrackerSettings& settings); + + static void webapiUpdateFeatureSettings( + SatelliteTrackerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + + bool isUpdatingSatData() { return m_updatingSatData; } + +private: + QThread m_thread; + SatelliteTrackerWorker *m_worker; + SatelliteTrackerSettings m_settings; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + HttpDownloadManager m_dlm; + bool m_updatingSatData; + int m_tleIndex; + QMutex m_mutex; + + QHash m_satellites; // Satellites, hashed on name + QHash m_satellitesId; // Same data as m_satellites, but hashed on id, rather than name + bool m_firstUpdateSatData; + + void start(); + void stop(); + void applySettings(const SatelliteTrackerSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const SatelliteTrackerSettings& settings, bool force); + + QString satNogsSatellitesFilename(); + QString satNogsTransmittersFilename(); + QString satNogsTLEFilename(); + QString tleURLToFilename(const QString& string); + bool parseSatellites(const QByteArray& json); + bool parseTransmitters(const QByteArray& json); + bool parseSatNogsTLEs(const QByteArray& json); + bool parseTxtTLEs(const QByteArray& txt); + bool readSatData(); + void updateSatData(); + void updateSatellitesReply(QNetworkReply *reply); + void updateTransmittersReply(QNetworkReply *reply); + void updateTLEsReply(QNetworkReply *reply); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void downloadFinished(const QString& filename, bool success); +}; + +#endif // INCLUDE_FEATURE_SATELLITETRACKER_H_ diff --git a/plugins/feature/satellitetracker/satellitetracker.qrc b/plugins/feature/satellitetracker/satellitetracker.qrc new file mode 100644 index 000000000..fc9e1498e --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetracker.qrc @@ -0,0 +1,6 @@ + + + satellitetracker/iss-32.png + satellitetracker/satellite-32.png + + diff --git a/plugins/feature/satellitetracker/satellitetracker/iss-32.png b/plugins/feature/satellitetracker/satellitetracker/iss-32.png new file mode 100644 index 000000000..ed1479e85 Binary files /dev/null and b/plugins/feature/satellitetracker/satellitetracker/iss-32.png differ diff --git a/plugins/feature/satellitetracker/satellitetracker/satellite-32.png b/plugins/feature/satellitetracker/satellitetracker/satellite-32.png new file mode 100644 index 000000000..6e6bd78c2 Binary files /dev/null and b/plugins/feature/satellitetracker/satellitetracker/satellite-32.png differ diff --git a/plugins/feature/satellitetracker/satellitetrackergui.cpp b/plugins/feature/satellitetracker/satellitetrackergui.cpp new file mode 100644 index 000000000..5f2998eef --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackergui.cpp @@ -0,0 +1,1168 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "feature/featureuiset.h" +#include "feature/featurewebapiutils.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "mainwindow.h" +#include "device/deviceuiset.h" +#include "util/units.h" +#include "util/astronomy.h" + +#include "ui_satellitetrackergui.h" +#include "satellitetracker.h" +#include "satellitetrackergui.h" +#include "satellitetrackerreport.h" +#include "satellitetrackersettingsdialog.h" +#include "satelliteselectiondialog.h" +#include "satelliteradiocontroldialog.h" +#include "satellitetrackersgp4.h" + +SatelliteTrackerGUI* SatelliteTrackerGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + SatelliteTrackerGUI* gui = new SatelliteTrackerGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void SatelliteTrackerGUI::destroy() +{ + delete this; +} + +void SatelliteTrackerGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray SatelliteTrackerGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool SatelliteTrackerGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + updateSelectedSats(); + displaySettings(); + qDebug() << " deserialize " << m_settings.m_satellites; + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +QString SatelliteTrackerGUI::convertDegreesToText(double degrees) +{ + if (m_settings.m_azElUnits == SatelliteTrackerSettings::DMS) + return Units::decimalDegreesToDegreeMinutesAndSeconds(degrees); + else if (m_settings.m_azElUnits == SatelliteTrackerSettings::DM) + return Units::decimalDegreesToDegreesAndMinutes(degrees); + else if (m_settings.m_azElUnits == SatelliteTrackerSettings::D) + return Units::decimalDegreesToDegrees(degrees); + else + return QString("%1").arg(degrees, 0, 'f', 2); +} + +bool SatelliteTrackerGUI::handleMessage(const Message& message) +{ + if (SatelliteTracker::MsgConfigureSatelliteTracker::match(message)) + { + qDebug("SatelliteTrackerGUI::handleMessage: SatelliteTracker::MsgConfigureSatelliteTracker"); + const SatelliteTracker::MsgConfigureSatelliteTracker& cfg = (SatelliteTracker::MsgConfigureSatelliteTracker&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (SatelliteTrackerReport::MsgReportSat::match(message)) + { + SatelliteTrackerReport::MsgReportSat& satReport = (SatelliteTrackerReport::MsgReportSat&) message; + SatelliteState *satState = satReport.getSatelliteState(); + if (satState->m_name == m_settings.m_target) + { + delete m_targetSatState; + m_targetSatState = satState; + + ui->azimuth->setText(convertDegreesToText(satState->m_azimuth)); + ui->elevation->setText(convertDegreesToText(satState->m_elevation)); + if (satState->m_passes.size() > 0) + { + SatellitePass *pass = satState->m_passes[0]; + bool geostationary = !pass->m_aos.isValid() && !pass->m_los.isValid(); + if ((m_nextTargetAOS != pass->m_aos) || (m_nextTargetLOS != pass->m_los) || (geostationary != m_geostationarySatVisible)) + { + m_nextTargetAOS = pass->m_aos; + m_nextTargetLOS = pass->m_los; + m_geostationarySatVisible = geostationary; + plotChart(); + updateTimeToAOS(); + } + } + } + updateTable(satState); + if (satState->m_name != m_settings.m_target) + delete satState; + return true; + } + else if (SatelliteTrackerReport::MsgReportAOS::match(message)) + { + SatelliteTrackerReport::MsgReportAOS& aosReport = (SatelliteTrackerReport::MsgReportAOS&) message; + aos(aosReport.getName(), aosReport.getDuration(), aosReport.getMaxElevation()); + return true; + } + else if (SatelliteTrackerReport::MsgReportTarget::match(message)) + { + SatelliteTrackerReport::MsgReportTarget& targetReport = (SatelliteTrackerReport::MsgReportTarget&) message; + setTarget(targetReport.getName()); + return true; + } + else if (SatelliteTrackerReport::MsgReportLOS::match(message)) + { + SatelliteTrackerReport::MsgReportLOS& losReport = (SatelliteTrackerReport::MsgReportLOS&) message; + los(losReport.getName()); + return true; + } + else if (SatelliteTracker::MsgSatData::match(message)) + { + SatelliteTracker::MsgSatData& satData = (SatelliteTracker::MsgSatData&) message; + m_satellites = satData.getSatellites(); + // Remove satellites that no longer exist + QMutableListIterator itr(m_settings.m_satellites); + while (itr.hasNext()) + { + QString satellite = itr.next(); + if (!m_satellites.contains(satellite)) + itr.remove(); + } + if (!m_satellites.contains(m_settings.m_target)) + setTarget(""); + // Update GUI + updateSelectedSats(); + return true; + } + + return false; +} + +// Call when m_settings.m_satellites changes +void SatelliteTrackerGUI::updateSelectedSats() +{ + // Remove unselects sats from target combo and table + for (int i = 0; i < ui->target->count(); ) + { + QString name = ui->target->itemText(i); + int idx = m_settings.m_satellites.indexOf(name); + if (idx == -1) + { + ui->target->removeItem(i); + QList matches = ui->satTable->findItems(name, Qt::MatchExactly); + for (int j = 0; j < matches.length(); j++) + ui->satTable->removeRow(ui->satTable->row(matches[j])); + } + else + i++; + } + // Add new satellites to target combo + for (int i = 0; i < m_settings.m_satellites.size(); i++) + { + if (ui->target->findText(m_settings.m_satellites[i], Qt::MatchExactly) == -1) + ui->target->addItem(m_settings.m_satellites[i]); + } + // Select current target, if it still exists + int idx = ui->target->findText(m_settings.m_target); + if (idx != -1) + ui->target->setCurrentIndex(idx); + else + setTarget(""); +} + +void SatelliteTrackerGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void SatelliteTrackerGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +SatelliteTrackerGUI::SatelliteTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::SatelliteTrackerGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0), + m_lastUpdatingSatData(false), + m_targetSatState(nullptr), + m_plotPass(0), + m_lineChart(nullptr), + m_polarChart(nullptr), + m_geostationarySatVisible(false) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + setChannelWidget(false); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + m_satelliteTracker = reinterpret_cast(feature); + m_satelliteTracker->setMessageQueueToGUI(&m_inputMessageQueue); + + m_featureUISet->addRollupWidget(this); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); + m_statusTimer.start(1000); + + // Intialise charts + m_emptyChart.layout()->setContentsMargins(0, 0, 0, 0); + m_emptyChart.setMargins(QMargins(1, 1, 1, 1)); + ui->passChart->setChart(&m_emptyChart); + ui->passChart->setRenderHint(QPainter::Antialiasing); + + ui->dateTime->setDateTime(QDateTime::currentDateTime()); + + // Use My Position from preferences, if none set + if ((m_settings.m_latitude == 0.0) && (m_settings.m_longitude == 0.0)) + on_useMyPosition_clicked(); + + resizeTable(); + // Allow user to reorder columns + ui->satTable->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->satTable->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + menu = new QMenu(ui->satTable); + for (int i = 0; i < ui->satTable->horizontalHeader()->count(); i++) + { + QString text = ui->satTable->horizontalHeaderItem(i)->text(); + menu->addAction(createCheckableItem(text, i, true)); + } + ui->satTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->satTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->satTable->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(satTable_sectionMoved(int, int, int))); + connect(ui->satTable->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(satTable_sectionResized(int, int, int))); + + m_speech = new QTextToSpeech(this); + + displaySettings(); + applySettings(true); + + // Get initial list of satellites + on_updateSatData_clicked(); +} + +SatelliteTrackerGUI::~SatelliteTrackerGUI() +{ + delete ui; +} + +void SatelliteTrackerGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void SatelliteTrackerGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + blockApplySettings(true); + ui->latitude->setValue(m_settings.m_latitude); + ui->longitude->setValue(m_settings.m_longitude); + ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target)); + if (m_settings.m_dateTime == "") + { + ui->dateTimeSelect->setCurrentIndex(0); + ui->dateTime->setVisible(false); + } + else + { + ui->dateTime->setDateTime(QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs)); + ui->dateTime->setVisible(true); + ui->dateTimeSelect->setCurrentIndex(1); + } + ui->autoTarget->setChecked(m_settings.m_autoTarget); + plotChart(); + blockApplySettings(false); +} + +void SatelliteTrackerGUI::leaveEvent(QEvent*) +{ +} + +void SatelliteTrackerGUI::enterEvent(QEvent*) +{ +} + +void SatelliteTrackerGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setColor(m_settings.m_rgbColor); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); + dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); + + dialog.move(p); + dialog.exec(); + + m_settings.m_rgbColor = dialog.getColor().rgb(); + m_settings.m_title = dialog.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); + m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + + resetContextMenuType(); +} + +void SatelliteTrackerGUI::aos(const QString& name, int duration, int maxElevation) +{ + // Give speech notification of pass + QString speech = m_settings.m_aosSpeech.trimmed(); + if (!speech.isEmpty()) + { + speech = speech.replace("${name}", name); + speech = speech.replace("${duration}", QString::number(duration)); + speech = speech.replace("${elevation}", QString::number(maxElevation)); + m_speech->say(speech); + } +} + +void SatelliteTrackerGUI::los(const QString& name) +{ + // Give speech notification of end of pass + QString speech = m_settings.m_losSpeech.trimmed(); + if (!speech.isEmpty()) + { + speech = speech.replace("${name}", name); + m_speech->say(speech); + } +} + +void SatelliteTrackerGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + SatelliteTracker::MsgStartStop *message = SatelliteTracker::MsgStartStop::create(checked); + m_satelliteTracker->getInputMessageQueue()->push(message); + } +} + +void SatelliteTrackerGUI::on_latitude_valueChanged(double value) +{ + m_settings.m_latitude = value; + applySettings(); + plotChart(); +} + +void SatelliteTrackerGUI::on_longitude_valueChanged(double value) +{ + m_settings.m_longitude = value; + applySettings(); + plotChart(); +} + +void SatelliteTrackerGUI::setTarget(const QString& target) +{ + if (target != m_settings.m_target) + { + m_settings.m_target = target; + ui->azimuth->setText(""); + ui->elevation->setText(""); + ui->aos->setText(""); + ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target)); + m_nextTargetAOS = QDateTime(); + m_nextTargetLOS = QDateTime(); + m_geostationarySatVisible = false; + applySettings(); + delete m_targetSatState; + m_targetSatState = nullptr; + m_plotPass = 0; + ui->passLabel->setText(QString("%1").arg(m_plotPass)); + plotChart(); + } +} + +void SatelliteTrackerGUI::on_target_currentTextChanged(const QString &text) +{ + setTarget(text); +} + +void SatelliteTrackerGUI::on_useMyPosition_clicked(bool checked) +{ + (void) checked; + double stationLatitude = MainCore::instance()->getSettings().getLatitude(); + double stationLongitude = MainCore::instance()->getSettings().getLongitude(); + double stationAltitude = MainCore::instance()->getSettings().getAltitude(); + + ui->latitude->setValue(stationLatitude); + ui->longitude->setValue(stationLongitude); + m_settings.m_heightAboveSeaLevel = stationAltitude; + applySettings(); + plotChart(); +} + +// Show settings dialog +void SatelliteTrackerGUI::on_displaySettings_clicked() +{ + SatelliteTrackerSettingsDialog dialog(&m_settings); + if (dialog.exec() == QDialog::Accepted) + { + applySettings(); + plotChart(); + } +} + +void SatelliteTrackerGUI::on_dateTimeSelect_currentTextChanged(const QString &text) +{ + if (text == "Now") + { + m_settings.m_dateTime = ""; + ui->dateTime->setVisible(false); + } + else + { + m_settings.m_dateTime = ui->dateTime->dateTime().toString(Qt::ISODateWithMs); + ui->dateTime->setVisible(true); + } + applySettings(); + plotChart(); +} + +void SatelliteTrackerGUI::on_dateTime_dateTimeChanged(const QDateTime &datetime) +{ + (void) datetime; + if (ui->dateTimeSelect->currentIndex() == 1) + { + m_settings.m_dateTime = ui->dateTime->dateTime().toString(Qt::ISODateWithMs); + applySettings(); + plotChart(); + } +} + +// Find target on the Map +void SatelliteTrackerGUI::on_viewOnMap_clicked() +{ + if (!m_settings.m_target.isEmpty()) + FeatureWebAPIUtils::mapFind(m_settings.m_target); +} + +void SatelliteTrackerGUI::on_updateSatData_clicked() +{ + m_satelliteTracker->getInputMessageQueue()->push(SatelliteTracker::MsgUpdateSatData::create()); +} + +void SatelliteTrackerGUI::on_selectSats_clicked() +{ + SatelliteSelectionDialog dialog(&m_settings, m_satellites); + if (dialog.exec() == QDialog::Accepted) + { + updateSelectedSats(); + applySettings(); + } +} + +void SatelliteTrackerGUI::on_radioControl_clicked() +{ + SatelliteRadioControlDialog dialog(&m_settings, m_satellites); + if (dialog.exec() == QDialog::Accepted) + applySettings(); +} + +void SatelliteTrackerGUI::on_autoTarget_clicked(bool checked) +{ + m_settings.m_autoTarget = checked; + applySettings(); +} + +void SatelliteTrackerGUI::updateStatus() +{ + int state = m_satelliteTracker->getState(); + + if (m_lastFeatureState != state) + { + // We set checked state of start/stop button, in case it was changed via API + bool oldState; + switch (state) + { + case Feature::StNotStarted: + ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case Feature::StIdle: + oldState = ui->startStop->blockSignals(true); + ui->startStop->setChecked(false); + ui->startStop->blockSignals(oldState); + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"); + break; + case Feature::StRunning: + oldState = ui->startStop->blockSignals(true); + ui->startStop->setChecked(true); + ui->startStop->blockSignals(oldState); + ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + break; + case Feature::StError: + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + QMessageBox::information(this, tr("Message"), m_satelliteTracker->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } + + // Indicate if satellite data is being updated + bool updatingSatData = m_satelliteTracker->isUpdatingSatData(); + if (updatingSatData != m_lastUpdatingSatData) + { + if (updatingSatData) + ui->updateSatData->setStyleSheet("QToolButton { background-color : green; }"); + else + ui->updateSatData->setStyleSheet("QToolButton { background: none; }"); + m_lastUpdatingSatData = updatingSatData; + } + + updateTimeToAOS(); +} + +// Update time to AOS +void SatelliteTrackerGUI::updateTimeToAOS() +{ + if (m_geostationarySatVisible) + ui->aos->setText("Now"); + else if (m_nextTargetAOS.isValid()) + { + QDateTime currentTime = QDateTime::currentDateTime(); + int secondsToAOS = m_nextTargetAOS.toSecsSinceEpoch() - currentTime.toSecsSinceEpoch(); + if (secondsToAOS > 0) + { + int seconds = secondsToAOS % 60; + int minutes = (secondsToAOS / 60) % 60; + int hours = (secondsToAOS / (60 * 60)) % 24; + int days = secondsToAOS / (60 * 60 * 24); + if (days == 1) + ui->aos->setText(QString("1 day")); + else if (days > 0) + ui->aos->setText(QString("%1 days").arg(days)); + else + { + ui->aos->setText(QString("%1:%2:%3") + .arg(hours, 2, 10, QLatin1Char('0')) + .arg(minutes, 2, 10, QLatin1Char('0')) + .arg(seconds, 2, 10, QLatin1Char('0'))); + } + } + else if (m_nextTargetLOS < currentTime) + ui->aos->setText(""); + else + ui->aos->setText("Now"); + } + else + ui->aos->setText(""); +} + +void SatelliteTrackerGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + SatelliteTracker::MsgConfigureSatelliteTracker* message = SatelliteTracker::MsgConfigureSatelliteTracker::create(m_settings, force); + m_satelliteTracker->getInputMessageQueue()->push(message); + } +} + +void SatelliteTrackerGUI::on_nextPass_clicked() +{ + if (m_targetSatState != nullptr) + { + if (m_plotPass < m_targetSatState->m_passes.size() - 1) + { + m_plotPass++; + ui->passLabel->setText(QString("%1").arg(m_plotPass)); + plotChart(); + } + } +} + +void SatelliteTrackerGUI::on_prevPass_clicked() +{ + if (m_plotPass > 0) + { + m_plotPass--; + ui->passLabel->setText(QString("%1").arg(m_plotPass)); + plotChart(); + } +} + +void SatelliteTrackerGUI::on_chartSelect_currentIndexChanged(int index) +{ + plotChart(); +} + +void SatelliteTrackerGUI::plotChart() +{ + if (ui->chartSelect->currentIndex() == 0) + plotPolarChart(); + else + plotAzElChart(); +} + +// Linear interpolation +static double interpolate(double x0, double y0, double x1, double y1, double x) +{ + return (y0*(x1-x) + y1*(x-x0)) / (x1-x0); +} + +static int findClosestPoint(qint64 v, QLineSeries *series) +{ + int i; + for (i = 0; i < series->count(); i++) + { + if (v < series->at(i).x()) + return i; + } + return i-1; +} + +// Plot pass in polar coords +void SatelliteTrackerGUI::plotPolarChart() +{ + if ((m_targetSatState == nullptr) || !m_satellites.contains(m_settings.m_target) || (m_targetSatState->m_passes.size() == 0)) + { + ui->passChart->setChart(&m_emptyChart); + return; + } + + QChart *oldChart = m_polarChart; + + if (m_plotPass >= m_targetSatState->m_passes.size() - 1) + { + m_plotPass = m_targetSatState->m_passes.size() - 1; + ui->passLabel->setText(QString("%1").arg(m_plotPass)); + } + SatellitePass *pass = m_targetSatState->m_passes[m_plotPass]; + + // Always create a new chart, otherwise sometimes they aren't drawn properly + m_polarChart = new QPolarChart(); + QValueAxis *angularAxis = new QValueAxis(); + QCategoryAxis *radialAxis = new QCategoryAxis(); + + angularAxis->setTickCount(9); + angularAxis->setMinorTickCount(1); + angularAxis->setLabelFormat("%d"); + angularAxis->setRange(0, 360); + + radialAxis->setMin(0); + radialAxis->setMax(90); + radialAxis->append("90", 0); + radialAxis->append("60", 30); + radialAxis->append("30", 60); + radialAxis->append("0", 90); + radialAxis->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue); + + m_polarChart->addAxis(angularAxis, QPolarChart::PolarOrientationAngular); + m_polarChart->addAxis(radialAxis, QPolarChart::PolarOrientationRadial); + m_polarChart->legend()->hide(); + m_polarChart->layout()->setContentsMargins(0, 0, 0, 0); + m_polarChart->setMargins(QMargins(1, 1, 1, 1)); + + SatNogsSatellite *sat = m_satellites.value(m_settings.m_target); + + if (pass->m_aos.isValid() && pass->m_los.isValid()) + { + QString title; + if (m_settings.m_utc) + title = pass->m_aos.date().toString(m_settings.m_dateFormat); + else + title = pass->m_aos.toLocalTime().date().toString(m_settings.m_dateFormat); + m_polarChart->setTitle(QString("%1").arg(title)); + + QLineSeries *polarSeries = new QLineSeries(); + + getPassAzEl(nullptr, nullptr, polarSeries, + sat->m_tle->m_tle0, sat->m_tle->m_tle1, sat->m_tle->m_tle2, + m_settings.m_latitude, m_settings.m_longitude, m_settings.m_heightAboveSeaLevel/1000.0, + pass->m_aos, pass->m_los); + + // Polar charts can't handle points that are more than 180 degrees apart, so + // we need to split passes that cross from 359 -> 0 degrees (or the reverse) + QList series; + series.append(new QLineSeries()); + QLineSeries *s = series.first(); + QPen pen(QColor(32, 159, 223), 2, Qt::SolidLine); + s->setPen(pen); + + qreal prevAz = polarSeries->at(0).x(); + qreal prevEl = polarSeries->at(0).y(); + for (int i = 1; i < polarSeries->count(); i++) + { + qreal az = polarSeries->at(i).x(); + qreal el = polarSeries->at(i).y(); + if ((prevAz > 270.0) && (az <= 90.0)) + { + double elMid = interpolate(prevAz, prevEl, az+360.0, el, 360.0); + s->append(360.0, elMid); + series.append(new QLineSeries()); + s = series.last(); + s->setPen(pen); + s->append(0.0, elMid); + s->append(az, el); + } + else if ((prevAz <= 90.0) && (az > 270.0)) + { + double elMid = interpolate(prevAz, prevEl, az-360.0, el, 0.0); + s->append(0.0, elMid); + series.append(new QLineSeries()); + s = series.last(); + s->setPen(pen); + s->append(360.0, elMid); + s->append(az, el); + } + else + s->append(polarSeries->at(i)); + prevAz = az; + prevEl = el; + } + + for (int i = 0; i < series.length(); i++) + { + m_polarChart->addSeries(series[i]); + series[i]->attachAxis(angularAxis); + series[i]->attachAxis(radialAxis); + } + + // Create series with single point, so we can plot time of AOS + QLineSeries *aosSeries = new QLineSeries(); + aosSeries->append(polarSeries->at(0)); + QTime time; + if (m_settings.m_utc) + time = pass->m_aos.time(); + else + time = pass->m_aos.toLocalTime().time(); + if (m_settings.m_utc) + aosSeries->setPointLabelsFormat(QString("AOS %1").arg(time.toString("hh:mm"))); + else + aosSeries->setPointLabelsFormat(QString("AOS %1").arg(time.toString("hh:mm"))); + aosSeries->setPointLabelsVisible(true); + aosSeries->setPointLabelsClipping(false); + m_polarChart->addSeries(aosSeries); + aosSeries->attachAxis(angularAxis); + aosSeries->attachAxis(radialAxis); + // Create series with single point, so we can plot time of LOS + QLineSeries *losSeries = new QLineSeries(); + losSeries->append(polarSeries->at(polarSeries->count()-1)); + if (m_settings.m_utc) + time = pass->m_los.time(); + else + time = pass->m_los.toLocalTime().time(); + losSeries->setPointLabelsFormat(QString("LOS %1").arg(time.toString("hh:mm"))); + losSeries->setPointLabelsVisible(true); + losSeries->setPointLabelsClipping(false); + m_polarChart->addSeries(losSeries); + losSeries->attachAxis(angularAxis); + losSeries->attachAxis(radialAxis); + + QDateTime currentTime; + if (m_settings.m_dateTime == "") + currentTime = QDateTime::currentDateTimeUtc(); + else if (m_settings.m_utc) + currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs); + else + currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs).toUTC(); + if ((currentTime >= pass->m_aos) && (currentTime <= pass->m_los)) + { + // Create series with single point, so we can plot current time + QLineSeries *nowSeries = new QLineSeries(); + // Find closest point to current time + int idx = std::round(polarSeries->count() * (currentTime.toMSecsSinceEpoch() - pass->m_aos.toMSecsSinceEpoch()) + / (pass->m_los.toMSecsSinceEpoch() - pass->m_aos.toMSecsSinceEpoch())); + nowSeries->append(polarSeries->at(idx)); + nowSeries->setPointLabelsFormat(m_settings.m_target); + nowSeries->setPointLabelsVisible(true); + nowSeries->setPointLabelsClipping(false); + m_polarChart->addSeries(nowSeries); + nowSeries->attachAxis(angularAxis); + nowSeries->attachAxis(radialAxis); + // Redraw in 5 seconds (call plotChart, incase user selects a different chart) + QTimer::singleShot(5000, this, &SatelliteTrackerGUI::plotChart); + } + + delete polarSeries; + } + else + { + // Possibly geostationary, just plot current position + QDateTime currentTime; + if (m_settings.m_dateTime == "") + currentTime = QDateTime::currentDateTimeUtc(); + else if (m_settings.m_utc) + currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs); + else + currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs).toUTC(); + QString title; + if (m_settings.m_utc) + title = currentTime.date().toString(m_settings.m_dateFormat); + else + title = currentTime.toLocalTime().date().toString(m_settings.m_dateFormat); + m_polarChart->setTitle(QString("%1").arg(title)); + + QLineSeries *nowSeries = new QLineSeries(); + + getPassAzEl(nullptr, nullptr, nowSeries, + sat->m_tle->m_tle0, sat->m_tle->m_tle1, sat->m_tle->m_tle2, + m_settings.m_latitude, m_settings.m_longitude, m_settings.m_heightAboveSeaLevel/1000.0, + currentTime, currentTime.addSecs(1)); + + nowSeries->setPointLabelsFormat(m_settings.m_target); + nowSeries->setPointLabelsVisible(true); + nowSeries->setPointLabelsClipping(false); + m_polarChart->addSeries(nowSeries); + nowSeries->attachAxis(angularAxis); + nowSeries->attachAxis(radialAxis); + } + + ui->passChart->setChart(m_polarChart); + + delete oldChart; +} + +// Plot target elevation/azimuth for the next pass +void SatelliteTrackerGUI::plotAzElChart() +{ + if ((m_targetSatState == nullptr) || !m_satellites.contains(m_settings.m_target) || (m_targetSatState->m_passes.size() == 0)) + { + ui->passChart->setChart(&m_emptyChart); + return; + } + + QChart *oldChart = m_lineChart; + + if (m_plotPass >= m_targetSatState->m_passes.size() - 1) + { + m_plotPass = m_targetSatState->m_passes.size() - 1; + ui->passLabel->setText(QString("%1").arg(m_plotPass)); + } + SatellitePass *pass = m_targetSatState->m_passes[m_plotPass]; + + // Always create a new chart, otherwise sometimes they aren't drawn properly + m_lineChart = new QChart(); + QDateTimeAxis *xAxis = new QDateTimeAxis(); + QValueAxis *yLeftAxis = new QValueAxis(); + QValueAxis *yRightAxis = new QValueAxis(); + + QString title; + if (m_settings.m_utc) + title = pass->m_aos.date().toString(m_settings.m_dateFormat); + else + title = pass->m_aos.toLocalTime().date().toString(m_settings.m_dateFormat); + m_lineChart->setTitle(QString("%1").arg(title)); + m_lineChart->legend()->hide(); + m_lineChart->addAxis(xAxis, Qt::AlignBottom); + m_lineChart->addAxis(yLeftAxis, Qt::AlignLeft); + m_lineChart->addAxis(yRightAxis, Qt::AlignRight); + m_lineChart->layout()->setContentsMargins(0, 0, 0, 0); + m_lineChart->setMargins(QMargins(1, 1, 1, 1)); + + SatNogsSatellite *sat = m_satellites.value(m_settings.m_target); + + QLineSeries *azSeries = new QLineSeries(); + QLineSeries *elSeries = new QLineSeries(); + + getPassAzEl(azSeries, elSeries, nullptr, + sat->m_tle->m_tle0, sat->m_tle->m_tle1, sat->m_tle->m_tle2, + m_settings.m_latitude, m_settings.m_longitude, m_settings.m_heightAboveSeaLevel/1000.0, + pass->m_aos, pass->m_los); + + // Split crossing of 360/0 degrees in to multiple series in the same colour + QList azSeriesList; + QPen pen(QColor(153, 202, 83), 2, Qt::SolidLine); + QLineSeries *s = new QLineSeries(); + azSeriesList.append(s); + s->setPen(pen); + qreal prevAz = azSeries->at(0).y(); + for (int i = 0; i < azSeries->count(); i++) + { + qreal az = azSeries->at(i).y(); + if (((prevAz >= 270) && (az < 90)) || ((prevAz < 90) && (az >= 270))) + { + s = new QLineSeries(); + azSeriesList.append(s); + s->setPen(pen); + } + s->append(azSeries->at(i).x(), az); + prevAz = az; + } + + m_lineChart->addSeries(elSeries); + elSeries->attachAxis(xAxis); + elSeries->attachAxis(yLeftAxis); + for (int i = 0; i < azSeriesList.size(); i++) + { + m_lineChart->addSeries(azSeriesList[i]); + azSeriesList[i]->attachAxis(xAxis); + azSeriesList[i]->attachAxis(yRightAxis); + } + xAxis->setRange(pass->m_aos, pass->m_los); + xAxis->setFormat("hh:mm"); + yLeftAxis->setRange(0.0, 90.0); + yLeftAxis->setTickCount(7); + yLeftAxis->setLabelFormat("%d"); + yLeftAxis->setTitleText(QString("Elevation (%1)").arg(QChar(0xb0))); + yRightAxis->setRange(0.0, 360.0); + yRightAxis->setTickCount(7); + yRightAxis->setLabelFormat("%d"); + yRightAxis->setTitleText(QString("Azimuth (%1)").arg(QChar(0xb0))); + + ui->passChart->setChart(m_lineChart); + + delete azSeries; + delete oldChart; +} + +void SatelliteTrackerGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + int row = ui->satTable->rowCount(); + ui->satTable->setRowCount(row + 1); + ui->satTable->setItem(row, SAT_COL_NAME, new QTableWidgetItem("Satellite123")); + ui->satTable->setItem(row, SAT_COL_AZ, new QTableWidgetItem("360")); + ui->satTable->setItem(row, SAT_COL_EL, new QTableWidgetItem("-90")); + ui->satTable->setItem(row, SAT_COL_AOS, new QTableWidgetItem("+1 10:17")); + ui->satTable->setItem(row, SAT_COL_LOS, new QTableWidgetItem("+1 10:17")); + ui->satTable->setItem(row, SAT_COL_MAX_EL, new QTableWidgetItem("90")); + ui->satTable->setItem(row, SAT_COL_DIR, new QTableWidgetItem("^")); + ui->satTable->setItem(row, SAT_COL_ALT, new QTableWidgetItem("50000")); + ui->satTable->setItem(row, SAT_COL_RANGE, new QTableWidgetItem("50000")); + ui->satTable->setItem(row, SAT_COL_RANGE_RATE, new QTableWidgetItem("10.0")); + ui->satTable->setItem(row, SAT_COL_DOPPLER, new QTableWidgetItem("10000")); + ui->satTable->setItem(row, SAT_COL_PATH_LOSS, new QTableWidgetItem("100")); + ui->satTable->setItem(row, SAT_COL_NORAD_ID, new QTableWidgetItem("123456")); + ui->satTable->resizeColumnsToContents(); + ui->satTable->setRowCount(row); +} + +// As we only have limited space in table, display time plus number of days to AOS/LOS +// unless it's greater than 10 days, in which case just display the date +QString SatelliteTrackerGUI::formatDaysTime(qint64 days, QDateTime dateTime) +{ + QDateTime dt; + if (m_settings.m_utc) + dt = dateTime.toUTC(); + else + dt = dateTime.toLocalTime(); + if (abs(days) > 10) + return dt.date().toString(m_settings.m_dateFormat); + else if (days == 0) + return dt.time().toString("hh:mm"); + else if (days > 0) + return dt.time().toString(QString("hh:mm +%1").arg(days)); + else + return dt.time().toString(QString("hh:mm %1").arg(days)); +} + +// Table item showing some text, but sorted by datetime set as user data +class DateTimeSortedTableWidgetItem : public QTableWidgetItem { +public: + bool operator<(const QTableWidgetItem& other) const + { + QVariant v1 = data(Qt::UserRole); + QVariant v2 = other.data(Qt::UserRole); + if (v1.isValid() && v2.isValid()) + return v1.toDateTime() < v2.toDateTime(); + else + return false; + } +}; + +#define SPEED_OF_LIGHT 299792458.0 + +// Frequency in Hz, speed in m/s +static double doppler(double frequency, double speed) +{ + return frequency * speed / SPEED_OF_LIGHT; +} + +// Frequency in Hz, speed in m/s +static double freeSpaceLoss(double frequency, double distance) +{ + return 20.0 * log10(distance) + 20 * log10(frequency) + 20 * log10(4*M_PI/SPEED_OF_LIGHT); +} + +// Distance in m, delay in s +static double propagationDelay(double distance) +{ + return distance / SPEED_OF_LIGHT; +} + +// Update satellite data table with latest data for the satellite +void SatelliteTrackerGUI::updateTable(SatelliteState *satState) +{ + // Does the table already contain this satellite? + QList matches = ui->satTable->findItems(satState->m_name, Qt::MatchExactly); + QTableWidgetItem *items[SAT_COL_COLUMNS]; + if (matches.size() == 0) + { + // Add a new row + int row = ui->satTable->rowCount(); + ui->satTable->setRowCount(row + 1); + for (int i = 0; i < SAT_COL_COLUMNS; i++) + { + if ((i == SAT_COL_AOS) || (i == SAT_COL_LOS)) + items[i] = new DateTimeSortedTableWidgetItem(); + else + items[i] = new QTableWidgetItem(); + items[i]->setToolTip(ui->satTable->horizontalHeaderItem(i)->toolTip()); + ui->satTable->setItem(row, i, items[i]); + } + // Static columns + items[SAT_COL_NAME]->setText(satState->m_name); + if (m_satellites.contains(satState->m_name)) + { + SatNogsSatellite *sat = m_satellites.value(satState->m_name); + items[SAT_COL_NORAD_ID]->setData(Qt::DisplayRole, sat->m_noradCatId); + } + } + else + { + // Update existing row + int row = ui->satTable->row(matches[0]); + for (int i = 0; i < SAT_COL_COLUMNS; i++) + items[i] = ui->satTable->item(row, i); + } + + items[SAT_COL_AZ]->setData(Qt::DisplayRole, (int)round(satState->m_azimuth)); + items[SAT_COL_EL]->setData(Qt::DisplayRole, (int)round(satState->m_elevation)); + if (satState->m_passes.size() > 0) + { + // Get number of days to AOS/LOS + QDate currentDate = QDate::currentDate(); + int daysToAOS = currentDate.daysTo(satState->m_passes[0]->m_aos.date()); + int daysToLOS = currentDate.daysTo(satState->m_passes[0]->m_los.date()); + items[SAT_COL_AOS]->setText(formatDaysTime(daysToAOS, satState->m_passes[0]->m_aos)); + items[SAT_COL_AOS]->setData(Qt::UserRole, satState->m_passes[0]->m_aos); + items[SAT_COL_LOS]->setText(formatDaysTime(daysToLOS, satState->m_passes[0]->m_los)); + items[SAT_COL_LOS]->setData(Qt::UserRole, satState->m_passes[0]->m_los); + items[SAT_COL_MAX_EL]->setData(Qt::DisplayRole, (int)round(satState->m_passes[0]->m_maxElevation)); + if (satState->m_passes[0]->m_northToSouth) + items[SAT_COL_DIR]->setText(QString("%1").arg(QChar(0x2193))); // Down arrow + else + items[SAT_COL_DIR]->setText(QString("%1").arg(QChar(0x2191))); // Up arrow + } + else + { + items[SAT_COL_AOS]->setText(""); + items[SAT_COL_LOS]->setText(""); + items[SAT_COL_MAX_EL]->setData(Qt::DisplayRole, QVariant()); + items[SAT_COL_DIR]->setText(""); + } + items[SAT_COL_ALT]->setData(Qt::DisplayRole, (int)round(satState->m_altitude)); + items[SAT_COL_RANGE]->setData(Qt::DisplayRole, (int)round(satState->m_range)); + items[SAT_COL_RANGE_RATE]->setData(Qt::DisplayRole, QString::number(satState->m_rangeRate, 'f', 3)); + items[SAT_COL_DOPPLER]->setData(Qt::DisplayRole, (int)round(-doppler(m_settings.m_defaultFrequency, satState->m_rangeRate*1000.0))); + items[SAT_COL_PATH_LOSS]->setData(Qt::DisplayRole, QString::number(freeSpaceLoss(m_settings.m_defaultFrequency, satState->m_range*1000.0), 'f', 1)); + items[SAT_COL_DELAY]->setData(Qt::DisplayRole, QString::number(propagationDelay(satState->m_range*1000.0)*1000.0, 'f', 1)); +} + +void SatelliteTrackerGUI::on_satTable_cellDoubleClicked(int row, int column) +{ + QString sat = ui->satTable->item(row, SAT_COL_NAME)->text(); + FeatureWebAPIUtils::mapFind(sat); +} + +// Columns in table reordered +void SatelliteTrackerGUI::satTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + m_settings.m_columnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void SatelliteTrackerGUI::satTable_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + m_settings.m_columnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void SatelliteTrackerGUI::columnSelectMenu(QPoint pos) +{ + menu->popup(ui->satTable->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void SatelliteTrackerGUI::columnSelectMenuChecked(bool checked) +{ + (void) checked; + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->satTable->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *SatelliteTrackerGUI::createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked())); + return action; +} diff --git a/plugins/feature/satellitetracker/satellitetrackergui.h b/plugins/feature/satellitetracker/satellitetrackergui.h new file mode 100644 index 000000000..f355e66a7 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackergui.h @@ -0,0 +1,154 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_SATELLITETRACKERGUI_H_ +#define INCLUDE_FEATURE_SATELLITETRACKERGUI_H_ + +#include +#include +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "satellitetrackersettings.h" +#include "satnogs.h" + +class PluginAPI; +class FeatureUISet; +class SatelliteTracker; +struct SatelliteState; + +namespace Ui { + class SatelliteTrackerGUI; +} + +using namespace QtCharts; + +class SatelliteTrackerGUI : public FeatureGUI { + Q_OBJECT +public: + static SatelliteTrackerGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + Ui::SatelliteTrackerGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + SatelliteTrackerSettings m_settings; + bool m_doApplySettings; + + SatelliteTracker* m_satelliteTracker; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + bool m_lastUpdatingSatData; + + QHash m_satellites; + SatelliteState *m_targetSatState; + + int m_plotPass; + + QChart m_emptyChart; + QChart *m_lineChart; + QPolarChart *m_polarChart; + + QDateTime m_nextTargetAOS; + QDateTime m_nextTargetLOS; + bool m_geostationarySatVisible; + + QTextToSpeech *m_speech; + QMenu *menu; // Column select context menu + + enum SatCol { + SAT_COL_NAME, + SAT_COL_AZ, + SAT_COL_EL, + SAT_COL_AOS, + SAT_COL_LOS, + SAT_COL_MAX_EL, + SAT_COL_DIR, + SAT_COL_ALT, + SAT_COL_RANGE, + SAT_COL_RANGE_RATE, + SAT_COL_DOPPLER, + SAT_COL_PATH_LOSS, + SAT_COL_DELAY, + SAT_COL_NORAD_ID + }; + + explicit SatelliteTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~SatelliteTrackerGUI(); + + void aos(const QString& name, int duration, int maxElevation); + void los(const QString& name); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void setTarget(const QString& target); + QString convertDegreesToText(double degrees); + bool handleMessage(const Message& message); + void plotChart(); + void plotAzElChart(); + void plotPolarChart(); + void resizeTable(); + void updateTable(SatelliteState *satState); + void updateSelectedSats(); + QAction *createCheckableItem(QString& text, int idx, bool checked); + void updateTimeToAOS(); + QString formatDaysTime(qint64 days, QDateTime dateTime); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_startStop_toggled(bool checked); + void on_useMyPosition_clicked(bool checked=false); + void on_latitude_valueChanged(double value); + void on_longitude_valueChanged(double value); + void on_target_currentTextChanged(const QString &text); + void on_displaySettings_clicked(); + void on_radioControl_clicked(); + void on_dateTimeSelect_currentTextChanged(const QString &text); + void on_dateTime_dateTimeChanged(const QDateTime &datetime); + void updateStatus(); + void on_viewOnMap_clicked(); + void on_updateSatData_clicked(); + void on_selectSats_clicked(); + void on_autoTarget_clicked(bool checked); + void on_chartSelect_currentIndexChanged(int index); + void on_nextPass_clicked(); + void on_prevPass_clicked(); + void on_satTable_cellDoubleClicked(int row, int column); + void satTable_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void satTable_sectionResized(int logicalIndex, int oldSize, int newSize); + void columnSelectMenu(QPoint pos); + void columnSelectMenuChecked(bool checked = false); +}; + + +#endif // INCLUDE_FEATURE_SATELLITETRACKERGUI_H_ diff --git a/plugins/feature/satellitetracker/satellitetrackergui.ui b/plugins/feature/satellitetracker/satellitetrackergui.ui new file mode 100644 index 000000000..ee816eca4 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackergui.ui @@ -0,0 +1,709 @@ + + + SatelliteTrackerGUI + + + + 0 + 0 + 525 + 750 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 16777215 + 16777215 + + + + + Liberation Sans + 9 + + + + Satellite Tracker + + + + + 10 + 10 + 301 + 141 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Longitude + + + + + + + Latitude + + + + + + + Date and time to use when calculating satellite's position + + + + Now + + + + + Custom + + + + + + + + Time to acquistion of signal (AOS) + + + + + + + Time + + + + + + + Latitude in decimal degrees (North positive) of antenna location + + + 6 + + + -90.000000000000000 + + + 90.000000000000000 + + + -90.000000000000000 + + + + + + + Elevation + + + + + + + Computed azimuth in degrees to the target satellite from the antenna's location + + + 360 + + + true + + + + + + + Target + + + + + + + + + Start/stop tracking + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Find target on the map + + + + + + + :/gridpolar.png:/gridpolar.png + + + + + + + Automatically select target satellite on AOS + + + ^ + + + + :/link.png:/link.png + + + true + + + true + + + + + + + Update satellite data + + + + + + + :/recycle.png:/recycle.png + + + + + + + SDRangel control + + + + + + + :/sdrangel_icon.png:/sdrangel_icon.png + + + + + + + Select satellites + + + + + + + :/gps.png:/gps.png + + + + + + + Set latitude, longitude and height from My Position in SDRangel preferences + + + + + + + :/import.png:/import.png + + + + + + + Show settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + + + Longitude in decimal degress (East positive) of antenna location + + + 6 + + + -180.000000000000000 + + + 180.000000000000000 + + + -180.000000000000000 + + + + + + + Computed elevation in degrees to the target satellite from the antenna's location + + + 90 + + + true + + + + + + + Azimuth + + + + + + + + 0 + 0 + + + + Target satellite + + + -1 + + + + + + + Date and time to use when calculating satellite's position + + + dd/MM/yyyy HH:mm:ss + + + true + + + + + + + AOS + + + + + + + + + + + 10 + 160 + 318 + 268 + + + + + 0 + 0 + + + + + 200 + 200 + + + + Pass Chart + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Select type of chart to plot + + + + Polar + + + + + Az/El vs Time + + + + + + + + Plot previous pass + + + ... + + + + :/arrow_right.png:/arrow_right.png + + + + + + + Plot next pass + + + + + + + :/arrow_left.png:/arrow_left.png + + + + + + + + 0 + 0 + + + + + 15 + 0 + + + + Pass number + + + 0 + + + + + + + + + + 300 + 250 + + + + Azimuth and elevation over time for satellite pass + + + + + + + + + 10 + 440 + 431 + 291 + + + + + 0 + 0 + + + + Satellite Data + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + + Satellite + + + Satellite name + + + + + Az + + + Azimuth in degrees to satellite from antenna location + + + + + El + + + Elevation in degrees to satellite from antenna location + + + + + AOS + + + Time of next AOS (Acquisition of signal) + + + + + LOS + + + Time of next LOS (Loss of signal) + + + + + Max El. + + + Maximum elevation in degrees of next satellite pass + + + + + Dir + + + Direction of the next pass + + + + + Alt (km) + + + Satellite altitude in kilometres + + + + + Range (km) + + + Range to satellite in kilometres + + + + + Range rate (km/s) + + + Speed of satellite towards antenna location in kilometers per second + + + + + Doppler (Hz) + + + Receive Doppler shift in Hertz (At frequency set in settings) + + + + + Path loss (dB) + + + Free space loss of signal in decibels (At frequency set in settings) + + + + + Delay (ms) + + + Propagation delay of a signal from the antenna to the satellite in milliseconds (assuming line-of-sight) + + + + + Norad ID + + + Norad catalog idenfitier for the satellite + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + QChartView + QGraphicsView +
QtCharts
+
+ + WrappingDateTimeEdit + QDateTimeEdit +
gui/wrappingdatetimeedit.h
+ 1 +
+
+ + startStop + viewOnMap + autoTarget + updateSatData + radioControl + selectSats + useMyPosition + displaySettings + latitude + longitude + dateTimeSelect + dateTime + target + aos + azimuth + elevation + chartSelect + prevPass + nextPass + passChart + satTable + + + + + +
diff --git a/plugins/feature/satellitetracker/satellitetrackerplugin.cpp b/plugins/feature/satellitetracker/satellitetrackerplugin.cpp new file mode 100644 index 000000000..225203662 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerplugin.cpp @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "satellitetrackergui.h" +#endif +#include "satellitetracker.h" +#include "satellitetrackerplugin.h" +#include "satellitetrackerwebapiadapter.h" + +const PluginDescriptor SatelliteTrackerPlugin::m_pluginDescriptor = { + SatelliteTracker::m_featureId, + QStringLiteral("Satellite Tracker"), + QStringLiteral("6.5.6"), + QStringLiteral("(c) Jon Beniston, M7RCE and Daniel Warner (SGP4 library)"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +SatelliteTrackerPlugin::SatelliteTrackerPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& SatelliteTrackerPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void SatelliteTrackerPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(SatelliteTracker::m_featureIdURI, SatelliteTracker::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* SatelliteTrackerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* SatelliteTrackerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return SatelliteTrackerGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* SatelliteTrackerPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new SatelliteTracker(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* SatelliteTrackerPlugin::createFeatureWebAPIAdapter() const +{ + return new SatelliteTrackerWebAPIAdapter(); +} diff --git a/plugins/feature/satellitetracker/satellitetrackerplugin.h b/plugins/feature/satellitetracker/satellitetrackerplugin.h new file mode 100644 index 000000000..96b0e8ab5 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_SATELLITETRACKERPLUGIN_H +#define INCLUDE_FEATURE_SATELLITETRACKERPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class SatelliteTrackerPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.satellitetracker") + +public: + explicit SatelliteTrackerPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const; + virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const; + virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_FEATURE_SATELLITETRACKERPLUGIN_H diff --git a/plugins/feature/satellitetracker/satellitetrackerreport.h b/plugins/feature/satellitetracker/satellitetrackerreport.h new file mode 100644 index 000000000..8dfad0c78 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerreport.h @@ -0,0 +1,130 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATELLITETRACKERREPORT_H_ +#define INCLUDE_FEATURE_SATELLITETRACKERREPORT_H_ + +#include + +#include "util/message.h" +#include "satellitetrackersgp4.h" + +class SatelliteTrackerReport : public QObject +{ + Q_OBJECT +public: + + // Sent from worker to GUI to give latest satellite data + class MsgReportSat : public Message { + MESSAGE_CLASS_DECLARATION + + public: + SatelliteState* getSatelliteState() const { return m_satState; } + + static MsgReportSat* create(SatelliteState* satState) + { + return new MsgReportSat(satState); + } + + private: + SatelliteState* m_satState; + + MsgReportSat(SatelliteState* satState) : + Message(), + m_satState(satState) + { + } + }; + + // Sent from worker to GUI to indicate AOS + class MsgReportAOS : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getName() const { return m_name; } + int getDuration() const { return m_duration; } + int getMaxElevation() const { return m_maxElevation; } + + static MsgReportAOS* create(const QString& name, int duration, int maxElevation) + { + return new MsgReportAOS(name, duration, maxElevation); + } + + private: + QString m_name; + int m_duration; + int m_maxElevation; + + MsgReportAOS(const QString& name, int duration, int maxElevation) : + Message(), + m_name(name), + m_duration(duration), + m_maxElevation(maxElevation) + { + } + }; + + // Sent from worker to GUI to indicaite LOS + class MsgReportLOS : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getName() const { return m_name; } + + static MsgReportLOS* create(const QString& name) + { + return new MsgReportLOS(name); + } + + private: + QString m_name; + + MsgReportLOS(const QString& name) : + Message(), + m_name(name) + { + } + }; + + // Sent from worker to GUI, to indicate target has changed + class MsgReportTarget : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getName() const { return m_name; } + + static MsgReportTarget* create(const QString& name) + { + return new MsgReportTarget(name); + } + + private: + QString m_name; + + MsgReportTarget(const QString& name) : + Message(), + m_name(name) + { + } + }; + +public: + SatelliteTrackerReport() {} + ~SatelliteTrackerReport() {} +}; + +#endif // INCLUDE_FEATURE_SATELLITETRACKERREPORT_H_ diff --git a/plugins/feature/satellitetracker/satellitetrackersettings.cpp b/plugins/feature/satellitetracker/satellitetrackersettings.cpp new file mode 100644 index 000000000..53fe10f14 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersettings.cpp @@ -0,0 +1,300 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "satellitetrackersettings.h" + +#define DEAFULT_TARGET "ISS" +#define DEFAULT_TLES {"https://db.satnogs.org/api/tle/", "https://www.amsat.org/tle/current/nasabare.txt", "https://www.celestrak.com/NORAD/elements/goes.txt"} +#define DEFAULT_DATE_FORMAT "yyyy/MM/dd" +#define DEFAULT_AOS_SPEECH "${name} is visible for ${duration} minutes. Max elevation, ${elevation} degrees." +#define DEFAULT_LOS_SPEECH "${name} is no longer visible." + +SatelliteTrackerSettings::SatelliteTrackerSettings() +{ + resetToDefaults(); +} + +void SatelliteTrackerSettings::resetToDefaults() +{ + m_latitude = 0.0; + m_longitude = 0.0; + m_heightAboveSeaLevel = 0.0; + m_target = DEAFULT_TARGET; + m_satellites = {QString(DEAFULT_TARGET)}; + m_tles = DEFAULT_TLES; + m_dateTime = ""; + m_minAOSElevation = 5; + m_minPassElevation = 15; + m_rotatorMaxAzimuth = 450; + m_rotatorMaxElevation = 180; + m_azElUnits = DM; + m_groundTrackPoints = 100; + m_dateFormat = DEFAULT_DATE_FORMAT; + m_utc = false; + m_updatePeriod = 1.0f; + m_dopplerPeriod = 10.0f; + m_defaultFrequency = 100000000.0f; + m_drawOnMap = true; + m_autoTarget = true; + m_aosSpeech = DEFAULT_AOS_SPEECH; + m_losSpeech = DEFAULT_LOS_SPEECH; + m_aosCommand = ""; + m_losCommand = ""; + m_predictionPeriod = 5; + m_passStartTime = QTime(0,0); + m_passFinishTime = QTime(23,59,59); + m_title = "Satellite Tracker"; + m_rgbColor = QColor(225, 25, 99).rgb(); + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; + for (int i = 0; i < SAT_COL_COLUMNS; i++) + { + m_columnIndexes[i] = i; + m_columnSizes[i] = -1; // Autosize + } +} + +QByteArray SatelliteTrackerSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeDouble(1, m_latitude); + s.writeDouble(2, m_longitude); + s.writeDouble(3, m_heightAboveSeaLevel); + s.writeString(4, m_target); + s.writeBlob(5, serializeStringList(m_satellites)); + s.writeBlob(6, serializeStringList(m_tles)); + s.writeString(7, m_dateTime); + s.writeS32(8, m_minAOSElevation); + s.writeS32(9, m_minPassElevation); + s.writeS32(10, m_rotatorMaxAzimuth); + s.writeS32(11, m_rotatorMaxElevation); + s.writeS32(12, m_azElUnits); + s.writeS32(13, m_groundTrackPoints); + s.writeString(14, m_dateFormat); + s.writeBool(15, m_utc); + s.writeFloat(16, m_updatePeriod); + s.writeFloat(17, m_dopplerPeriod); + s.writeS32(18, m_predictionPeriod); + s.writeString(19, m_passStartTime.toString()); + s.writeString(20, m_passFinishTime.toString()); + s.writeFloat(21, m_defaultFrequency); + s.writeBool(22, m_drawOnMap); + s.writeBool(23, m_autoTarget); + s.writeString(24, m_aosSpeech); + s.writeString(25, m_losSpeech); + s.writeString(26, m_aosCommand); + s.writeString(27, m_losCommand); + s.writeBlob(28, serializeDeviceSettings(m_deviceSettings)); + + s.writeString(29, m_title); + s.writeU32(30, m_rgbColor); + s.writeBool(31, m_useReverseAPI); + s.writeString(32, m_reverseAPIAddress); + s.writeU32(33, m_reverseAPIPort); + s.writeU32(34, m_reverseAPIFeatureSetIndex); + s.writeU32(35, m_reverseAPIFeatureIndex); + + for (int i = 0; i < SAT_COL_COLUMNS; i++) + s.writeS32(100 + i, m_columnIndexes[i]); + for (int i = 0; i < SAT_COL_COLUMNS; i++) + s.writeS32(200 + i, m_columnSizes[i]); + + return s.final(); +} + +bool SatelliteTrackerSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + QByteArray blob; + + d.readDouble(1, &m_latitude, 0.0); + d.readDouble(2, &m_longitude, 0.0); + d.readDouble(3, &m_heightAboveSeaLevel, 0.0); + d.readString(4, &m_target, DEAFULT_TARGET); + d.readBlob(5, &blob); + deserializeStringList(blob, m_satellites); + d.readBlob(6, &blob); + deserializeStringList(blob, m_tles); + d.readString(7, &m_dateTime, ""); + d.readS32(8, &m_minAOSElevation, 5); + d.readS32(9, &m_minPassElevation, 15); + d.readS32(10, &m_rotatorMaxAzimuth, 450); + d.readS32(11, &m_rotatorMaxElevation, 180); + d.readS32(12, (qint32 *)&m_azElUnits, DM); + d.readS32(13, &m_groundTrackPoints, 100); + d.readString(14, &m_dateFormat, DEFAULT_DATE_FORMAT); + d.readBool(15, &m_utc, false); + d.readFloat(16, &m_updatePeriod, 1.0f); + d.readFloat(17, &m_dopplerPeriod, 10.0f); + d.readS32(18, &m_predictionPeriod, 5); + d.readString(19, &strtmp, "00:00:00"); + m_passStartTime = QTime::fromString(strtmp); + d.readString(20, &strtmp, "23:59:59"); + m_passFinishTime = QTime::fromString(strtmp); + d.readFloat(21, &m_defaultFrequency, 100000000.0f); + d.readBool(22, &m_drawOnMap, true); + d.readBool(23, &m_autoTarget, true); + d.readString(24, &m_aosSpeech, DEFAULT_AOS_SPEECH); + d.readString(25, &m_aosCommand, DEFAULT_LOS_SPEECH); + d.readString(26, &m_aosCommand, ""); + d.readString(27, &m_losCommand, ""); + d.readBlob(28, &blob); + deserializeDeviceSettings(blob, m_deviceSettings); + + d.readString(29, &m_title, "Satellite Tracker"); + d.readU32(30, &m_rgbColor, QColor(225, 25, 99).rgb()); + d.readBool(31, &m_useReverseAPI, false); + d.readString(32, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(33, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(34, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(35, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + for (int i = 0; i < SAT_COL_COLUMNS; i++) + d.readS32(100 + i, &m_columnIndexes[i], i); + for (int i = 0; i < SAT_COL_COLUMNS; i++) + d.readS32(200 + i, &m_columnSizes[i], -1); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +QByteArray SatelliteTrackerSettings::serializeStringList(const QList& strings) const +{ + QByteArray data; + QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly); + (*stream) << strings; + delete stream; + return data; +} + +void SatelliteTrackerSettings::deserializeStringList(const QByteArray& data, QList& strings) +{ + QDataStream *stream = new QDataStream(data); + (*stream) >> strings; + delete stream; +} + +QDataStream& operator<<(QDataStream& out, const QList *list) +{ + out << *list; + return out; +} + +QDataStream& operator>>(QDataStream& in, QList*& list) +{ + list = new QList(); + in >> *list; + return in; +} + +QDataStream& operator<<(QDataStream& out, const SatelliteTrackerSettings::SatelliteDeviceSettings* settings) +{ + out << settings->m_deviceSet; + out << settings->m_presetGroup; + out << settings->m_presetFrequency; + out << settings->m_presetDescription; + out << settings->m_doppler; + out << settings->m_startOnAOS; + out << settings->m_stopOnLOS; + out << settings->m_startStopFileSink; + out << settings->m_frequency; + out << settings->m_aosCommand; + out << settings->m_losCommand; + return out; +} + +QDataStream& operator>>(QDataStream& in, SatelliteTrackerSettings::SatelliteDeviceSettings*& settings) +{ + settings = new SatelliteTrackerSettings::SatelliteDeviceSettings(); + in >> settings->m_deviceSet; + in >> settings->m_presetGroup; + in >> settings->m_presetFrequency; + in >> settings->m_presetDescription; + in >> settings->m_doppler; + in >> settings->m_startOnAOS; + in >> settings->m_stopOnLOS; + in >> settings->m_startStopFileSink; + in >> settings->m_frequency; + in >> settings->m_aosCommand; + in >> settings->m_losCommand; + return in; +} + +QByteArray SatelliteTrackerSettings::serializeDeviceSettings(QHash *> deviceSettings) const +{ + QByteArray data; + QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly); + (*stream) << deviceSettings; + delete stream; + return data; +} + +void SatelliteTrackerSettings::deserializeDeviceSettings(const QByteArray& data, QHash *>& deviceSettings) +{ + QDataStream *stream = new QDataStream(data); + (*stream) >> deviceSettings; + delete stream; +} + +SatelliteTrackerSettings::SatelliteDeviceSettings::SatelliteDeviceSettings() +{ + m_deviceSet = "R0"; + m_presetFrequency = 0; + m_startOnAOS = true; + m_stopOnLOS = true; + m_startStopFileSink = true; + m_frequency = 0; + m_aosCommand = ""; + m_losCommand = ""; +} diff --git a/plugins/feature/satellitetracker/satellitetrackersettings.h b/plugins/feature/satellitetracker/satellitetrackersettings.h new file mode 100644 index 000000000..6caa0bdd9 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersettings.h @@ -0,0 +1,100 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_SATELLITETRACKERSETTINGS_H_ +#define INCLUDE_FEATURE_SATELLITETRACKERSETTINGS_H_ + +#include +#include +#include +#include +#include + +class Serializable; + +#define SAT_COL_COLUMNS 14 + +struct SatelliteTrackerSettings +{ + struct SatelliteDeviceSettings + { + QString m_deviceSet; //!< R0, T1... + QString m_presetGroup; //!< Preset to load to device set + quint64 m_presetFrequency; + QString m_presetDescription; + QList m_doppler; //!< Which channels to apply Doppler correction to, if any + bool m_startOnAOS; //!< Start acquistion on AOS + bool m_stopOnLOS; //!< Stop acquistion on LOS + bool m_startStopFileSink; //!< Start&stop file sinks recording on AOS/LOS + quint64 m_frequency; //!< Optional center frequency to set (in Hz), to override preset value + QString m_aosCommand; //!< Command/script to execute on AOS + QString m_losCommand; //!< Command/script to execute on LOS + SatelliteDeviceSettings(); + }; + + double m_latitude; //!< Antenna location, degrees + double m_longitude; + double m_heightAboveSeaLevel; //!< In metres + QString m_target; //!< Target satellite + QList m_satellites; //!< Selected satellites + QList m_tles; //!< TLE URLs + QString m_dateTime; //!< Date/time for observation, or "" for now (UTC or local as per m_utc) + int m_minAOSElevation; //!< Minimum elevation for AOS + int m_minPassElevation; //!< Minimum elevation for a pass + int m_rotatorMaxAzimuth; //!< Maximum rotator azimuth 360/450 + int m_rotatorMaxElevation; //!< Maximum rotator elevation 90/180 + enum AzElUnits {DMS, DM, D, Decimal} m_azElUnits; + int m_groundTrackPoints; //!< Number of points in ground tracks + QString m_dateFormat; //!< Format used for displaying dates in the GUI + bool m_utc; //!< Set/display times as UTC rather than local + float m_updatePeriod; //!< How long in seconds between updates of satellite's position + float m_dopplerPeriod; //!< How long in seconds between Doppler corrections + int m_predictionPeriod; //!< How many days ahead to predict passes in + QTime m_passStartTime; //!< Time after which pass must start + QTime m_passFinishTime; //!< Time before which pass must finish + float m_defaultFrequency; //!< Frequency used for Doppler & path loss calculation in satellite table + bool m_drawOnMap; + bool m_autoTarget; //!< Automatically select target on AOS + QString m_aosSpeech; //!< Text to say on AOS + QString m_losSpeech; //!< Text to say on LOS + QString m_aosCommand; //!< Command/script to execute on AOS + QString m_losCommand; //!< Command/script to execute on LOS + QHash *> m_deviceSettings; //!< Settings for each device set for each satellite + + int m_columnIndexes[SAT_COL_COLUMNS];//!< How the columns are ordered in the table + int m_columnSizes[SAT_COL_COLUMNS]; //!< Size of the coumns in the table + + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + + SatelliteTrackerSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + QByteArray serializeStringList(const QList& strings) const; + void deserializeStringList(const QByteArray& data, QList& strings); + QByteArray serializeDeviceSettings(QHash *> deviceSettings) const; + void deserializeDeviceSettings(const QByteArray& data, QHash *>& deviceSettings); +}; + +#endif // INCLUDE_FEATURE_SATELLITETRACKERSETTINGS_H_ diff --git a/plugins/feature/satellitetracker/satellitetrackersettingsdialog.cpp b/plugins/feature/satellitetracker/satellitetrackersettingsdialog.cpp new file mode 100644 index 000000000..c1e53b568 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersettingsdialog.cpp @@ -0,0 +1,97 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 "satellitetrackersettingsdialog.h" +#include + +SatelliteTrackerSettingsDialog::SatelliteTrackerSettingsDialog(SatelliteTrackerSettings *settings, + QWidget* parent) : + QDialog(parent), + m_settings(settings), + ui(new Ui::SatelliteTrackerSettingsDialog) +{ + ui->setupUi(this); + ui->height->setValue(settings->m_heightAboveSeaLevel); + ui->predictionPeriod->setValue(settings->m_predictionPeriod); + ui->passStartTime->setTime(settings->m_passStartTime); + ui->passFinishTime->setTime(settings->m_passFinishTime); + ui->minimumAOSElevation->setValue(settings->m_minAOSElevation); + ui->minimumPassElevation->setValue(settings->m_minPassElevation); + ui->aosSpeech->setText(settings->m_aosSpeech); + ui->losSpeech->setText(settings->m_losSpeech); + ui->aosCommand->setText(settings->m_aosCommand); + ui->losCommand->setText(settings->m_losCommand); + ui->updatePeriod->setValue(settings->m_updatePeriod); + ui->dopplerPeriod->setValue(settings->m_dopplerPeriod); + ui->defaultFrequency->setValue(settings->m_defaultFrequency / 1000000.0); + ui->azElUnits->setCurrentIndex((int)settings->m_azElUnits); + ui->groundTrackPoints->setValue(settings->m_groundTrackPoints); + ui->dateFormat->setText(settings->m_dateFormat); + ui->utc->setChecked(settings->m_utc); + ui->drawOnMap->setChecked(settings->m_drawOnMap); + for (int i = 0; i < settings->m_tles.size(); i++) + { + QListWidgetItem *item = new QListWidgetItem(settings->m_tles[i]); + item->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEditable|Qt::ItemIsEnabled); + ui->tles->addItem(item); + } +} + +SatelliteTrackerSettingsDialog::~SatelliteTrackerSettingsDialog() +{ + delete ui; +} + +void SatelliteTrackerSettingsDialog::on_addTle_clicked() +{ + QListWidgetItem *item = new QListWidgetItem("http://"); + item->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEditable|Qt::ItemIsEnabled); + ui->tles->addItem(item); +} + +void SatelliteTrackerSettingsDialog::on_removeTle_clicked() +{ + QList items = ui->tles->selectedItems(); + for (int i = 0; i < items.size(); i++) + delete items[i]; +} + +void SatelliteTrackerSettingsDialog::accept() +{ + m_settings->m_heightAboveSeaLevel = ui->height->value(); + m_settings->m_predictionPeriod = ui->predictionPeriod->value(); + m_settings->m_passStartTime = ui->passStartTime->time(); + m_settings->m_passFinishTime = ui->passFinishTime->time(); + m_settings->m_minAOSElevation = ui->minimumAOSElevation->value(); + m_settings->m_minPassElevation = ui->minimumPassElevation->value(); + m_settings->m_aosSpeech = ui->aosSpeech->text(); + m_settings->m_losSpeech = ui->losSpeech->text(); + m_settings->m_aosCommand = ui->aosCommand->text(); + m_settings->m_losCommand = ui->losCommand->text(); + m_settings->m_updatePeriod = (float)ui->updatePeriod->value(); + m_settings->m_dopplerPeriod = (float)ui->dopplerPeriod->value(); + m_settings->m_defaultFrequency = (float)(ui->defaultFrequency->value() * 1000000.0); + m_settings->m_azElUnits = (SatelliteTrackerSettings::AzElUnits)ui->azElUnits->currentIndex(); + m_settings->m_groundTrackPoints = ui->groundTrackPoints->value(); + m_settings->m_dateFormat = ui->dateFormat->text(); + m_settings->m_utc = ui->utc->isChecked(); + m_settings->m_drawOnMap = ui->drawOnMap->isChecked(); + m_settings->m_tles.clear(); + for (int i = 0; i < ui->tles->count(); i++) + m_settings->m_tles.append(ui->tles->item(i)->text()); + QDialog::accept(); +} diff --git a/plugins/feature/satellitetracker/satellitetrackersettingsdialog.h b/plugins/feature/satellitetracker/satellitetrackersettingsdialog.h new file mode 100644 index 000000000..da8e3a111 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersettingsdialog.h @@ -0,0 +1,42 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATELLITETRACKERSETTINGSDIALOG_H +#define INCLUDE_SATELLITETRACKERSETTINGSDIALOG_H + +#include "ui_satellitetrackersettingsdialog.h" +#include "satellitetrackersettings.h" + +class SatelliteTrackerSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit SatelliteTrackerSettingsDialog(SatelliteTrackerSettings* settings, QWidget* parent = 0); + ~SatelliteTrackerSettingsDialog(); + + SatelliteTrackerSettings *m_settings; + +private slots: + void on_addTle_clicked(); + void on_removeTle_clicked(); + void accept(); + +private: + Ui::SatelliteTrackerSettingsDialog* ui; +}; + +#endif // INCLUDE_SATELLITETRACKERSETTINGSDIALOG_H diff --git a/plugins/feature/satellitetracker/satellitetrackersettingsdialog.ui b/plugins/feature/satellitetracker/satellitetrackersettingsdialog.ui new file mode 100644 index 000000000..08d13515b --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersettingsdialog.ui @@ -0,0 +1,576 @@ + + + SatelliteTrackerSettingsDialog + + + + 0 + 0 + 487 + 543 + + + + + Liberation Sans + 9 + + + + Satellite Tracker Settings + + + + + + + + + 0 + + + + Passes + + + + + + Antenna height (m ASL) + + + + + + + Height of antenna location above sea level in metres + + + -1000 + + + 20000 + + + + + + + Number of days ahead for which passes should be predicted in + + + 1 + + + 365 + + + + + + + Minimum elevation for AOS (°) + + + + + + + Enter a minimum elevation in degrees for which AOS (Acquisition of Signal) will be indicated + + + -90 + + + 90 + + + + + + + Minimum elevation for pass (°) + + + + + + + Enter a minimum elevation in degrees a satellite must reach in a pass + + + 90 + + + + + + + Passes must start after + + + + + + + + + + Passes must finish before + + + + + + + + + + + + + + Rotator maximum azimuth (°) + + + + + + + Maximum azimuth angle of rotator in degrees + + + 360 + + + 720 + + + 90 + + + 450 + + + + + + + Rotator maximum elevation (°) + + + + + + + Maximum elevation angle of rotator in degrees + + + 180 + + + 90 + + + 180 + + + + + + + AOS speech warning + + + + + + + Text to say when a satellite signal is acquired + + + + + + + LOS speech warning + + + + + + + Text to say when a satellite signal is lost + + + + + + + AOS command + + + + + + + Program / script to execute on AOS + + + + + + + LOS command + + + + + + + Program / script to execute on LOS + + + + + + + Doppler period (s) + + + + + + + Enter the time in seconds between each Doppler correction + + + 0.010000000000000 + + + 100.000000000000000 + + + 10.000000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Prediction period (days) + + + + + + + + TLEs + + + + + + Satellite Two Line Element (TLE) sources + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + + + + + + + Add TLE + + + + + + + + + + + Remove selected TLE + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Display + + + + + + Frequency used for Doppler and free space path loss calculations in the satellite table + + + 3 + + + 1.000000000000000 + + + 50000.000000000000000 + + + 100.000000000000000 + + + 100.000000000000000 + + + + + + + Format for dates displayed in the GUI + + + + + + + When checked satellite positions are sent to the map + + + Draw satellites on map + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Update period (s) + + + + + + + Enter the time in seconds between each calculation of the target's position + + + 3600.000000000000000 + + + 1.000000000000000 + + + + + + + Default frequency (MHz) + + + + + + + Units used for displaying azimuth and elevation. Either degrees, minutes and seconds or decimal degrees. + + + false + + + + ° ' " + + + + + ° ' + + + + + ° + + + + + Decimal + + + + + + + + Azimuth and elevation units + + + + + + + When checked times are dispayed using UTC rather than the local time zone + + + Display times in UTC + + + + + + + Date format + + + + + + + Ground track points + + + + + + + Number of points in ground tracks (more points result in smoother curves) + + + 10 + + + 360 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + tabWidget + height + predictionPeriod + minimumAOSElevation + minimumPassElevation + passStartTime + passFinishTime + rotatorMaximumAzimuth + rotatorMaximumElevation + aosSpeech + losSpeech + aosCommand + losCommand + dopplerPeriod + tles + addTle + removeTle + updatePeriod + defaultFrequency + azElUnits + groundTrackPoints + dateFormat + utc + drawOnMap + + + + + buttonBox + accepted() + SatelliteTrackerSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SatelliteTrackerSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/satellitetracker/satellitetrackersgp4.cpp b/plugins/feature/satellitetracker/satellitetrackersgp4.cpp new file mode 100644 index 000000000..be98cee29 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersgp4.cpp @@ -0,0 +1,497 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2013 Daniel Warner // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include + +#include "util/units.h" + +#include "satellitetrackersgp4.h" + +// Convert QGP4 DateTime to Qt QDataTime +static QDateTime dateTimeToQDateTime(DateTime dt) +{ + QDateTime qdt(QDate(dt.Year(), dt.Month(), dt.Day()), QTime(dt.Hour(), dt.Minute(), dt.Second(), (int)(dt.Microsecond()/1000.0)), Qt::UTC); + return qdt; +} + +// Convert Qt QDataTime to QGP4 DateTime +static DateTime qDateTimeToDateTime(QDateTime qdt) +{ + QDateTime utc = qdt.toUTC(); + QDate date = utc.date(); + QTime time = utc.time(); + DateTime dt(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second()); + return dt; +} + +// Get ground track +// Throws SatelliteException, DecayedException and TleException +void getGroundTrack(QDateTime dateTime, + const QString& tle0, const QString& tle1, const QString& tle2, + int steps, bool forward, + QList& coordinates) +{ + Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString()); + SGP4 sgp4(tle); + OrbitalElements ele(tle); + double periodMins; + double timeStep; + + // Note map doesn't support paths wrapping around Earth + DateTime currentTime = qDateTimeToDateTime(dateTime); + DateTime endTime; + if (forward) + { + periodMins = ele.Period() * 0.9; + endTime = currentTime.AddMinutes(periodMins); + timeStep = periodMins / (steps * 0.9); + } + else + { + periodMins = ele.Period() * 0.4; + endTime = currentTime.AddMinutes(-periodMins); + timeStep = -periodMins / (steps * 0.4); + } + + coordinates.clear(); + while (forward && (currentTime < endTime) || !forward && (currentTime > endTime)) + { + // Calculate satellite position + Eci eci = sgp4.FindPosition(currentTime); + + // Convert satellite position to geodetic coordinates (lat and long) + CoordGeodetic geo = eci.ToGeodetic(); + + QGeoCoordinate *coord = new QGeoCoordinate(Units::radiansToDegrees(geo.latitude), + Units::radiansToDegrees(geo.longitude), + geo.altitude * 1000.0); + coordinates.append(coord); + // Map is stretched at poles, so use finer steps + if (std::abs(Units::radiansToDegrees(geo.latitude)) >= 70) + currentTime = currentTime.AddMinutes(timeStep/4); + else + currentTime = currentTime.AddMinutes(timeStep); + } +} + +// Find azimuth and elevation points during a pass +void getPassAzEl(QLineSeries* azimuth, QLineSeries* elevation, QLineSeries* polar, + const QString& tle0, const QString& tle1, const QString& tle2, + double latitude, double longitude, double altitude, + QDateTime& aos, QDateTime& los) +{ + try + { + Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString()); + SGP4 sgp4(tle); + Observer obs(latitude, longitude, altitude); + + DateTime aosTime = qDateTimeToDateTime(aos); + DateTime losTime = qDateTimeToDateTime(los); + DateTime currentTime(aosTime); + int steps = 20; + + double timeStep = (losTime - aosTime).TotalSeconds() / steps; + + while (currentTime <= losTime) + { + // Calculate satellite position + Eci eci = sgp4.FindPosition(currentTime); + + // Calculate angle to satellite from antenna + CoordTopocentric topo = obs.GetLookAngle(eci); + + // Save azimuth and elevation in series + QDateTime qdt = dateTimeToQDateTime(currentTime); + if (azimuth != nullptr) + azimuth->append(qdt.toMSecsSinceEpoch(), Units::radiansToDegrees(topo.azimuth)); + if (elevation != nullptr) + elevation->append(qdt.toMSecsSinceEpoch(), Units::radiansToDegrees(topo.elevation)); + if (polar != nullptr) + polar->append(Units::radiansToDegrees(topo.azimuth), 90.0-Units::radiansToDegrees(topo.elevation)); + + currentTime = currentTime.AddSeconds(timeStep); + } + } + catch (SatelliteException se) + { + qDebug() << se.what(); + } + catch (DecayedException de) + { + qDebug() << de.what(); + } + catch (TleException tlee) + { + qDebug() << tlee.what(); + } +} + +// Get whether a pass passes through 0 degreees +bool getPassesThrough0Deg(const QString& tle0, const QString& tle1, const QString& tle2, + double latitude, double longitude, double altitude, + QDateTime& aos, QDateTime& los) +{ + try + { + Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString()); + SGP4 sgp4(tle); + Observer obs(latitude, longitude, altitude); + + DateTime aosTime = qDateTimeToDateTime(aos); + DateTime losTime = qDateTimeToDateTime(los); + DateTime currentTime(aosTime); + int steps = 20; + + double timeStep = (losTime - aosTime).TotalSeconds() / steps; + + double prevAz; + for (int i = 0; i < steps; i++) + { + // Calculate satellite position + Eci eci = sgp4.FindPosition(currentTime); + + // Calculate angle to satellite from antenna + CoordTopocentric topo = obs.GetLookAngle(eci); + + double az = Units::radiansToDegrees(topo.azimuth); + if (i == 0) + prevAz = az; + + // Does it cross 0 degrees? + if (((prevAz > 270.0) && (az < 90.0)) || ((prevAz < 90.0) && (az >= 270.0))) + return true; + + prevAz = az; + currentTime = currentTime.AddSeconds(timeStep); + } + } + catch (SatelliteException se) + { + qDebug() << se.what(); + } + catch (DecayedException de) + { + qDebug() << de.what(); + } + catch (TleException tlee) + { + qDebug() << tlee.what(); + } + return false; +} + +// Find maximum elevation in a pass +static double findMaxElevation(Observer& obs1, SGP4& sgp4, const DateTime& aos, const DateTime& los) +{ + Observer obs(obs1.GetLocation()); + bool running; + double timeStep = (los - aos).TotalSeconds() / 9.0; + DateTime currentTime(aos); + DateTime time1(aos); + DateTime time2(los); + double maxElevation; + + do + { + running = true; + maxElevation = -INFINITY; + while (running && (currentTime < time2)) + { + Eci eci = sgp4.FindPosition(currentTime); + CoordTopocentric topo = obs.GetLookAngle(eci); + if (topo.elevation > maxElevation) + { + maxElevation = topo.elevation; + currentTime = currentTime.AddSeconds(timeStep); + if (currentTime > time2) + currentTime = time2; + } + else + running = false; + } + time1 = currentTime.AddSeconds(-2.0 * timeStep); + time2 = currentTime; + currentTime = time1; + timeStep = (time2 - time1).TotalSeconds() / 9.0; + } + while (timeStep > 1.0); + + return Units::radiansToDegrees(maxElevation); +} + +// Find the time at which the satellite crossed the minimum elevation required for AOS or LOS +static DateTime findCrossingPoint(Observer& obs, SGP4& sgp4, const DateTime& initialTime1, const DateTime& initialTime2, double minElevation, bool findingAOS) +{ + bool running; + int cnt; + DateTime time1(initialTime1); + DateTime time2(initialTime2); + DateTime middleTime; + + running = true; + cnt = 0; + while (running && (cnt++ < 16)) + { + middleTime = time1.AddSeconds((time2 - time1).TotalSeconds() / 2.0); + Eci eci = sgp4.FindPosition(middleTime); + CoordTopocentric topo = obs.GetLookAngle(eci); + if (topo.elevation > minElevation) + { + if (findingAOS) + time2 = middleTime; + else + time1 = middleTime; + } + else + { + if (findingAOS) + time1 = middleTime; + else + time2 = middleTime; + } + if ((time2 - time1).TotalSeconds() < 1.0) + { + running = false; + int us = middleTime.Microsecond(); + middleTime = middleTime.AddMicroseconds(-us); + middleTime = middleTime.AddSeconds(findingAOS ? 1 : -1); + } + } + running = true; + cnt = 0; + while (running && (cnt++ < 6)) + { + Eci eci = sgp4.FindPosition(middleTime); + CoordTopocentric topo = obs.GetLookAngle(eci); + if (topo.elevation > minElevation) + middleTime = middleTime.AddSeconds(findingAOS ? -1 : 1); + else + running = false; + } + return middleTime; +} + +// Find when AOS occured, by stepping backwards +static DateTime findAOSBackwards(Observer& obs, SGP4& sgp4, DateTime& startTime, + int predictionPeriod, double minElevation, bool& aosUnknown) +{ + DateTime previousTime(startTime); + DateTime currentTime(startTime); + DateTime endTime(startTime.AddDays(-predictionPeriod)); + + while (currentTime >= endTime) + { + Eci eci = sgp4.FindPosition(currentTime); + CoordTopocentric topo = obs.GetLookAngle(eci); + if (topo.elevation < minElevation) + { + aosUnknown = false; + return findCrossingPoint(obs, sgp4, currentTime, previousTime, minElevation, true); + } + previousTime = currentTime; + currentTime = currentTime - TimeSpan(0, 0, 180); + } + aosUnknown = true; + return currentTime; +} + +bool inPassWindow(DateTime dateTime, QTime passStartTime, QTime passEndTime, bool utc) +{ + // Don't compare seconds as not currently settable in GUI + QDateTime qdt = dateTimeToQDateTime(dateTime); + if (!utc) + qdt = qdt.toLocalTime(); + QTime qt(qdt.time().hour(), qdt.time().minute()); + passStartTime = QTime(passStartTime.hour(), passStartTime.minute()); + passEndTime = QTime(passEndTime.hour(), passEndTime.minute()); + // If passEndTime is before passStartTime, then we allow overnight passes + if (passEndTime > passStartTime) + { + return (qt >= passStartTime) && (qt <= passEndTime); + } + else + { + return (qt <= passEndTime) || (qt >= passStartTime); + } +} + +// Create a list of satellite passes, between the given start and end times, that exceed the specified minimum elevation +// We return an uninitalised QDateTime if AOS or LOS is outside of predictionPeriod +static QList createPassList(Observer& obs, SGP4& sgp4, DateTime& startTime, + int predictionPeriod, double minAOSElevation, double minPassElevationDeg, + QTime passStartTime, QTime passEndTime, bool utc, + int noOfPasses) +{ + QList passes; + bool aos = false; + bool aosUnknown = true; + double aosAz; + double losAz; + DateTime previousTime(startTime); + DateTime currentTime(startTime); + DateTime endTime(startTime.AddDays(predictionPeriod)); + DateTime aosTime; + DateTime losTime; + + while (currentTime < endTime) + { + bool endOfPass = false; + Eci eci = sgp4.FindPosition(currentTime); + CoordTopocentric topo = obs.GetLookAngle(eci); + + if (!aos && (topo.elevation > minAOSElevation)) + { + if (startTime == currentTime) + { + // AOS is before startTime + aosTime = findAOSBackwards(obs, sgp4, startTime, predictionPeriod, minAOSElevation, aosUnknown); + } + else + { + aosTime = findCrossingPoint(obs, sgp4, previousTime, currentTime, minAOSElevation, true); + aosUnknown = false; + } + aos = true; + eci = sgp4.FindPosition(aosTime); + topo = obs.GetLookAngle(eci); + aosAz = Units::radiansToDegrees(topo.azimuth); + } + else if (aos && (topo.elevation < minAOSElevation)) + { + aos = false; + endOfPass = true; + losTime = findCrossingPoint(obs, sgp4, previousTime, currentTime, minAOSElevation, false); + eci = sgp4.FindPosition(losTime); + topo = obs.GetLookAngle(eci); + losAz = Units::radiansToDegrees(topo.azimuth); + double maxElevationDeg = findMaxElevation(obs, sgp4, aosTime, losTime); + if ((maxElevationDeg >= minPassElevationDeg) + && inPassWindow(aosTime, passStartTime, passEndTime, utc) + && inPassWindow(losTime, passStartTime, passEndTime, utc)) + { + SatellitePass *pass = new SatellitePass; + pass->m_aos = aosUnknown ? QDateTime() : dateTimeToQDateTime(aosTime); + pass->m_los = dateTimeToQDateTime(losTime); + pass->m_maxElevation = maxElevationDeg; + pass->m_aosAzimuth = aosAz; + pass->m_losAzimuth = losAz; + pass->m_northToSouth = std::min(360.0-aosAz, aosAz-0.0) < std::min(360.0-losAz, losAz-0.0); + passes.append(pass); + noOfPasses--; + if (noOfPasses <= 0) + return passes; + } + } + previousTime = currentTime; + if (endOfPass) + currentTime = currentTime + TimeSpan(0, 30, 0); // 30 minutes - no orbit likely to be that fast + else + currentTime = currentTime + TimeSpan(0, 0, 180); + if (currentTime > endTime) + currentTime = endTime; + } + if (aos) + { + // Pass still in progress at end time + Eci eci = sgp4.FindPosition(currentTime); + CoordTopocentric topo = obs.GetLookAngle(eci); + losAz = Units::radiansToDegrees(topo.azimuth); + double maxElevationDeg = findMaxElevation(obs, sgp4, aosTime, losTime); + if ((maxElevationDeg >= minPassElevationDeg) + && inPassWindow(aosTime, passStartTime, passEndTime, utc) + && inPassWindow(losTime, passStartTime, passEndTime, utc)) + { + SatellitePass *pass = new SatellitePass; + pass->m_aos = aosUnknown ? QDateTime() : dateTimeToQDateTime(aosTime); + pass->m_los = QDateTime(); + pass->m_aosAzimuth = aosAz; + pass->m_losAzimuth = losAz; + pass->m_maxElevation = maxElevationDeg; + pass->m_northToSouth = std::min(360.0-aosAz, aosAz-0.0) < std::min(360.0-losAz, losAz-0.0); + passes.append(pass); + } + } + + return passes; +} + +void getSatelliteState(QDateTime dateTime, + const QString& tle0, const QString& tle1, const QString& tle2, + double latitude, double longitude, double altitude, + int predictionPeriod, int minAOSElevationDeg, int minPassElevationDeg, + QTime passStartTime, QTime passFinishTime, bool utc, + int noOfPasses, int groundTrackSteps, SatelliteState *satState) +{ + try { + Tle tle = Tle(tle0.toStdString(), tle1.toStdString(), tle2.toStdString()); + SGP4 sgp4(tle); + Observer obs(latitude, longitude, altitude); + + DateTime dt = qDateTimeToDateTime(dateTime); + + // Calculate satellite position + Eci eci = sgp4.FindPosition(dt); + + // Calculate angle to satellite from antenna + CoordTopocentric topo = obs.GetLookAngle(eci); + + // Convert satellite position to geodetic coordinates (lat and long) + CoordGeodetic geo = eci.ToGeodetic(); + + satState->m_latitude = Units::radiansToDegrees(geo.latitude); + satState->m_longitude = Units::radiansToDegrees(geo.longitude); + satState->m_altitude = geo.altitude; + satState->m_azimuth = Units::radiansToDegrees(topo.azimuth); + satState->m_elevation = Units::radiansToDegrees(topo.elevation); + satState->m_range = topo.range; + satState->m_rangeRate = topo.range_rate; + OrbitalElements ele(tle); + satState->m_speed = eci.Velocity().Magnitude(); + satState->m_period = ele.Period(); + if (noOfPasses > 0) + { + qDeleteAll(satState->m_passes); + satState->m_passes = createPassList(obs, sgp4, dt, predictionPeriod, + Units::degreesToRadians((double)minAOSElevationDeg), + minPassElevationDeg, + passStartTime, passFinishTime, utc, + noOfPasses); + } + + getGroundTrack(dateTime, tle0, tle1, tle2, groundTrackSteps, false, satState->m_groundTrack); + getGroundTrack(dateTime, tle0, tle1, tle2, groundTrackSteps, true, satState->m_predictedGroundTrack); + } + catch (SatelliteException se) + { + qDebug() << "getSatelliteState: " << satState->m_name << ": " << se.what(); + } + catch (DecayedException de) + { + qDebug() << "getSatelliteState: " << satState->m_name << ": " << de.what(); + } + catch (TleException tlee) + { + qDebug() << "getSatelliteState: " << satState->m_name << ": " << tlee.what(); + } +} diff --git a/plugins/feature/satellitetracker/satellitetrackersgp4.h b/plugins/feature/satellitetracker/satellitetrackersgp4.h new file mode 100644 index 000000000..a27ab249f --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackersgp4.h @@ -0,0 +1,73 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATELLITETRACKERSGP4_H_ +#define INCLUDE_FEATURE_SATELLITETRACKERSGP4_H_ + +#include +#include +#include +#include + +using namespace QtCharts; + +struct SatellitePass { + QDateTime m_aos; + QDateTime m_los; + double m_maxElevation; // Degrees + double m_aosAzimuth; // Degrees + double m_losAzimuth; // Degrees + bool m_northToSouth; +}; + +struct SatelliteState { + QString m_name; + double m_latitude; // Degrees + double m_longitude; // Degrees + double m_altitude; // km + double m_azimuth; // Degrees + double m_elevation; // Degrees + double m_range; // km + double m_rangeRate; // km/s + double m_speed; + double m_period; + QList m_passes; + QList m_groundTrack; + QList m_predictedGroundTrack; +}; + +void getGroundTrack(QDateTime dateTime, + const QString& tle0, const QString& tle1, const QString& tle2, + int steps, QList& coordinates); + +void getSatelliteState(QDateTime dateTime, + const QString& tle0, const QString& tle1, const QString& tle2, + double latitude, double longitude, double altitude, + int predictionPeriod, int minAOSElevationDeg, int minPassElevationDeg, + QTime passStartTime, QTime passFinishTime, bool utc, + int noOfPasses, int groundTrackSteps, SatelliteState *satState); + +void getPassAzEl(QLineSeries *azimuth, QLineSeries *elevation, QLineSeries *polar, + const QString& tle0, const QString& tle1, const QString& tle2, + double latitude, double longitude, double altitude, + QDateTime& aos, QDateTime& los); + +bool getPassesThrough0Deg(const QString& tle0, const QString& tle1, const QString& tle2, + double latitude, double longitude, double altitude, + QDateTime& aos, QDateTime& los); + +#endif // INCLUDE_FEATURE_SATELLITETRACKERSGP4_H_ diff --git a/plugins/feature/satellitetracker/satellitetrackerwebapiadapter.cpp b/plugins/feature/satellitetracker/satellitetrackerwebapiadapter.cpp new file mode 100644 index 000000000..37ec7602e --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGFeatureSettings.h" +#include "satellitetracker.h" +#include "satellitetrackerwebapiadapter.h" + +SatelliteTrackerWebAPIAdapter::SatelliteTrackerWebAPIAdapter() +{} + +SatelliteTrackerWebAPIAdapter::~SatelliteTrackerWebAPIAdapter() +{} + +int SatelliteTrackerWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSatelliteTrackerSettings(new SWGSDRangel::SWGSatelliteTrackerSettings()); + response.getSatelliteTrackerSettings()->init(); + SatelliteTracker::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int SatelliteTrackerWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + SatelliteTracker::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/satellitetracker/satellitetrackerwebapiadapter.h b/plugins/feature/satellitetracker/satellitetrackerwebapiadapter.h new file mode 100644 index 000000000..d287e995f --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_SATELLITETRACKER_WEBAPIADAPTER_H +#define INCLUDE_SATELLITETRACKER_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "satellitetrackersettings.h" + +/** + * Standalone API adapter only for the settings + */ +class SatelliteTrackerWebAPIAdapter : public FeatureWebAPIAdapter { +public: + SatelliteTrackerWebAPIAdapter(); + virtual ~SatelliteTrackerWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + +private: + SatelliteTrackerSettings m_settings; +}; + +#endif // INCLUDE_SATELLITETRACKER_WEBAPIADAPTER_H diff --git a/plugins/feature/satellitetracker/satellitetrackerworker.cpp b/plugins/feature/satellitetracker/satellitetrackerworker.cpp new file mode 100644 index 000000000..f6e695c19 --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerworker.cpp @@ -0,0 +1,826 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "SWGTargetAzimuthElevation.h" +#include "SWGMapItem.h" + +#include "webapi/webapiadapterinterface.h" +#include "webapi/webapiutils.h" + +#include "util/units.h" +#include "device/deviceset.h" +#include "device/deviceapi.h" +#include "channel/channelwebapiutils.h" +#include "maincore.h" + +#include "satellitetracker.h" +#include "satellitetrackerworker.h" +#include "satellitetrackerreport.h" +#include "satellitetrackersgp4.h" + +MESSAGE_CLASS_DEFINITION(SatelliteTrackerWorker::MsgConfigureSatelliteTrackerWorker, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTrackerReport::MsgReportSat, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTrackerReport::MsgReportAOS, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTrackerReport::MsgReportLOS, Message) +MESSAGE_CLASS_DEFINITION(SatelliteTrackerReport::MsgReportTarget, Message) + +SatelliteTrackerWorker::SatelliteTrackerWorker(SatelliteTracker* satelliteTracker, WebAPIAdapterInterface *webAPIAdapterInterface) : + m_satelliteTracker(satelliteTracker), + m_webAPIAdapterInterface(webAPIAdapterInterface), + m_msgQueueToFeature(nullptr), + m_msgQueueToGUI(nullptr), + m_running(false), + m_mutex(QMutex::Recursive), + m_recalculatePasses(true), + m_flipRotation(false), + m_extendedAzRotation(false) +{ + connect(&m_pollTimer, SIGNAL(timeout()), this, SLOT(update())); +} + +SatelliteTrackerWorker::~SatelliteTrackerWorker() +{ + m_inputMessageQueue.clear(); +} + +void SatelliteTrackerWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +bool SatelliteTrackerWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_pollTimer.start((int)round(m_settings.m_updatePeriod*1000.0)); + // Resume doppler timers + QHashIterator itr(m_workerState); + while (itr.hasNext()) + { + itr.next(); + SatWorkerState *satWorkerState = itr.value(); + if (satWorkerState->m_dopplerTimer.interval() > 0) + satWorkerState->m_dopplerTimer.start(); + } + m_recalculatePasses = true; + m_running = true; + return m_running; +} + +void SatelliteTrackerWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_pollTimer.stop(); + // Stop doppler timers + QHashIterator itr(m_workerState); + while (itr.hasNext()) + { + itr.next(); + itr.value()->m_dopplerTimer.stop(); + } + m_running = false; +} + +void SatelliteTrackerWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool SatelliteTrackerWorker::handleMessage(const Message& message) +{ + if (MsgConfigureSatelliteTrackerWorker::match(message)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureSatelliteTrackerWorker& cfg = (MsgConfigureSatelliteTrackerWorker&) message; + + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else if (SatelliteTracker::MsgSatData::match(message)) + { + SatelliteTracker::MsgSatData& satData = (SatelliteTracker::MsgSatData&) message; + m_satellites = satData.getSatellites(); + m_recalculatePasses = true; + return true; + } + else + { + return false; + } +} + +void SatelliteTrackerWorker::applySettings(const SatelliteTrackerSettings& settings, bool force) +{ + qDebug() << "SatelliteTrackerWorker::applySettings:" + << " m_target: " << settings.m_target + << " m_satellites: " << settings.m_satellites + << " m_dateTime: " << settings.m_dateTime + << " m_utc: " << settings.m_utc + << " m_updatePeriod: " << settings.m_updatePeriod + << " force: " << force; + + if ((m_settings.m_target != settings.m_target) + || (m_settings.m_latitude != settings.m_latitude) + || (m_settings.m_longitude != settings.m_longitude) + || (m_settings.m_heightAboveSeaLevel != settings.m_heightAboveSeaLevel) + || (m_settings.m_dateTime != settings.m_dateTime) + || (m_settings.m_utc != settings.m_utc) + || (m_settings.m_groundTrackPoints != settings.m_groundTrackPoints) + || (m_settings.m_minAOSElevation != settings.m_minAOSElevation) + || (m_settings.m_minPassElevation != settings.m_minPassElevation) + || (m_settings.m_predictionPeriod != settings.m_predictionPeriod) + || (m_settings.m_passStartTime != settings.m_passStartTime) + || (m_settings.m_passFinishTime != settings.m_passFinishTime) + || (!m_settings.m_drawOnMap && settings.m_drawOnMap) + || force) + { + // Recalculate immediately + m_recalculatePasses = true; + QTimer::singleShot(1, this, &SatelliteTrackerWorker::update); + m_pollTimer.start((int)round(settings.m_updatePeriod*1000.0)); + } + else if ((m_settings.m_updatePeriod != settings.m_updatePeriod) || force) + { + m_pollTimer.start((int)round(settings.m_updatePeriod*1000.0)); + } + + if (!settings.m_drawOnMap && m_settings.m_drawOnMap) + { + QHashIterator itr(m_workerState); + while (itr.hasNext()) + { + itr.next(); + removeFromMap(itr.key()); + } + } + + // Remove satellites no longer needed + QMutableHashIterator itr(m_workerState); + while (itr.hasNext()) + { + itr.next(); + if (settings.m_satellites.indexOf(itr.key()) == -1) + itr.remove(); + } + + // Add new satellites + for (int i = 0; i < settings.m_satellites.size(); i++) + { + if (!m_workerState.contains(settings.m_satellites[i])) + { + SatWorkerState *satWorkerState = new SatWorkerState(settings.m_satellites[i]); + m_workerState.insert(settings.m_satellites[i], satWorkerState); + connect(&satWorkerState->m_aosTimer, &QTimer::timeout, [this, satWorkerState]() { + aos(satWorkerState); + }); + connect(&satWorkerState->m_losTimer, &QTimer::timeout, [this, satWorkerState]() { + los(satWorkerState); + }); + m_recalculatePasses = true; + } + } + + m_settings = settings; +} + +void SatelliteTrackerWorker::removeFromMap(QString id) +{ + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_satelliteTracker, "mapitems"); + if (mapMessageQueues) + sendToMap(mapMessageQueues, id, "", "", 0.0, 0.0, 0.0, 0.0, nullptr, nullptr); +} + +void SatelliteTrackerWorker::sendToMap(QList *mapMessageQueues, + QString name, QString image, QString text, + double lat, double lon, double altitude, double rotation, + QList *track, QList *predictedTrack) +{ + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setLatitude(lat); + swgMapItem->setLongitude(lon); + swgMapItem->setAltitude(altitude); + swgMapItem->setImage(new QString(image)); + swgMapItem->setImageRotation(rotation); + swgMapItem->setText(new QString(text)); + swgMapItem->setImageMinZoom(0); + if (track != nullptr) + { + QList *mapTrack = new QList(); + for (int i = 0; i < track->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = new SWGSDRangel::SWGMapCoordinate(); + QGeoCoordinate *c = track->at(i); + p->setLatitude(c->latitude()); + p->setLongitude(c->longitude()); + p->setAltitude(c->altitude()); + mapTrack->append(p); + } + swgMapItem->setTrack(mapTrack); + } + if (predictedTrack != nullptr) + { + QList *mapTrack = new QList(); + for (int i = 0; i < predictedTrack->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = new SWGSDRangel::SWGMapCoordinate(); + QGeoCoordinate *c = predictedTrack->at(i); + p->setLatitude(c->latitude()); + p->setLongitude(c->longitude()); + p->setAltitude(c->altitude()); + mapTrack->append(p); + } + swgMapItem->setPredictedTrack(mapTrack); + } + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_satelliteTracker, swgMapItem); + (*it)->push(msg); + } +} + +void SatelliteTrackerWorker::update() +{ + // Get date and time to calculate position at + QDateTime qdt; + if (m_settings.m_dateTime == "") + qdt = QDateTime::currentDateTimeUtc(); + else if (m_settings.m_utc) + qdt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs); + else + qdt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs).toUTC(); + + QHashIterator itr(m_workerState); + while (itr.hasNext()) + { + itr.next(); + SatWorkerState *satWorkerState = itr.value(); + QString name = satWorkerState->m_name; + if (m_satellites.contains(name)) + { + SatNogsSatellite *sat = m_satellites.value(name); + if (sat->m_tle != nullptr) + { + // Calculate position, AOS/LOS and other details for satellite + int noOfPasses; + bool recalcAsPastLOS = (satWorkerState->m_satState.m_passes.size() > 0) && (satWorkerState->m_satState.m_passes[0]->m_los < qdt); + if (m_recalculatePasses || recalcAsPastLOS) + noOfPasses = (name == m_settings.m_target) ? 99 : 1; + else + noOfPasses = 0; + getSatelliteState(qdt, sat->m_tle->m_tle0, sat->m_tle->m_tle1, sat->m_tle->m_tle2, + m_settings.m_latitude, m_settings.m_longitude, m_settings.m_heightAboveSeaLevel/1000.0, + m_settings.m_predictionPeriod, m_settings.m_minAOSElevation, m_settings.m_minPassElevation, + m_settings.m_passStartTime, m_settings.m_passFinishTime, m_settings.m_utc, + noOfPasses, m_settings.m_groundTrackPoints, &satWorkerState->m_satState); + + // Update AOS/LOS (only set timers if using real time) + if ((m_settings.m_dateTime == "") && (satWorkerState->m_satState.m_passes.size() > 0)) + { + /*int min = 8; + QDateTime p1a = QDateTime(QDateTime::currentDateTime().date(), QTime(16, min, 0)); + QDateTime p1s = QDateTime(QDateTime::currentDateTime().date(), QTime(16, min, 30)); + + QDateTime p2a = QDateTime(QDateTime::currentDateTime().date(), QTime(16, min+1, 0)); + QDateTime p2s = QDateTime(QDateTime::currentDateTime().date(), QTime(16, min+1, 30)); + + if (qdt > p1a) + { + satWorkerState->m_satState.m_passes[0]->m_aos = p2a; + satWorkerState->m_satState.m_passes[0]->m_los = p2s; + } + else + { + satWorkerState->m_satState.m_passes[0]->m_aos = p1a; + satWorkerState->m_satState.m_passes[0]->m_los = p1s; + } */ + + + /* if (name == "NOAA 18") + { + satWorkerState->m_satState.m_passes[0]->m_aos = QDateTime(QDateTime::currentDateTime().date(), QTime(11, 10, 0)); + satWorkerState->m_satState.m_passes[0]->m_los = QDateTime(QDateTime::currentDateTime().date(), QTime(11, 10, 30)); + } */ + + + /*if (name == "ISS") + { + if (m_settings.m_minAOSElevation == 5) + { + qDebug() << "*********** seting first AOS"; + satWorkerState->m_satState.m_passes[0]->m_aos = QDateTime(QDateTime::currentDateTime().date(), QTime(14, 10, 0)); + satWorkerState->m_satState.m_passes[0]->m_los = QDateTime(QDateTime::currentDateTime().date(), QTime(14, 10, 30)); + } + else + { + qDebug() << "*********** seting second AOS"; + satWorkerState->m_satState.m_passes[0]->m_aos = QDateTime(QDateTime::currentDateTime().date(), QTime(14, 11, 0)); + satWorkerState->m_satState.m_passes[0]->m_los = QDateTime(QDateTime::currentDateTime().date(), QTime(14, 11, 30)); + } + }*/ + + + // Do we have a new AOS? + if ((satWorkerState->m_aos != satWorkerState->m_satState.m_passes[0]->m_aos) || (satWorkerState->m_los != satWorkerState->m_satState.m_passes[0]->m_los)) + { + qDebug() << "New AOS: " << name << " new: " << satWorkerState->m_satState.m_passes[0]->m_aos << " old: " << satWorkerState->m_aos; + qDebug() << "New LOS: " << name << " new: " << satWorkerState->m_satState.m_passes[0]->m_los << " old: " << satWorkerState->m_los; + satWorkerState->m_aos = satWorkerState->m_satState.m_passes[0]->m_aos; + satWorkerState->m_los = satWorkerState->m_satState.m_passes[0]->m_los; + if (satWorkerState->m_aos.isValid()) + { + if (satWorkerState->m_aos > qdt) + { + satWorkerState->m_aosTimer.setInterval(satWorkerState->m_aos.toMSecsSinceEpoch() - qdt.toMSecsSinceEpoch()); + satWorkerState->m_aosTimer.setSingleShot(true); + satWorkerState->m_aosTimer.start(); + } + else if (qdt < satWorkerState->m_los) + aos(satWorkerState); + + if (satWorkerState->m_los.isValid() && (m_settings.m_target == satWorkerState->m_name)) + calculateRotation(satWorkerState); + } + if (satWorkerState->m_los.isValid() && (satWorkerState->m_los > qdt)) + { + if (satWorkerState->m_losTimer.isActive() && (satWorkerState->m_losTimer.remainingTime() == 0)) + { + qDebug() << "****** m_losTimer.remainingTime: " << satWorkerState->m_losTimer.remainingTime(); + qDebug() << "****** m_losTimer.active: " << satWorkerState->m_losTimer.isActive(); + // LOS hasn't been called yet - do so, before we reset timer + los(satWorkerState); + } + satWorkerState->m_losTimer.setInterval(satWorkerState->m_los.toMSecsSinceEpoch() - qdt.toMSecsSinceEpoch()); + satWorkerState->m_losTimer.setSingleShot(true); + satWorkerState->m_losTimer.start(); + } + } + } + else + { + satWorkerState->m_aos = QDateTime(); + satWorkerState->m_los = QDateTime(); + satWorkerState->m_aosTimer.stop(); + satWorkerState->m_losTimer.stop(); + } + + // Send Az/El of target to Rotator Controllers, if elevation above horizon + if ((name == m_settings.m_target) && (satWorkerState->m_satState.m_elevation >= 0)) + { + double azimuth = satWorkerState->m_satState.m_azimuth; + double elevation = satWorkerState->m_satState.m_elevation; + if (m_extendedAzRotation) + { + if (azimuth < 180.0) + azimuth += 360.0; + } + else if (m_flipRotation) + { + azimuth = std::fmod(azimuth + 180.0, 360.0); + elevation = 180.0 - elevation; + } + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *rotatorMessageQueues = messagePipes.getMessageQueues(m_satelliteTracker, "target"); + if (rotatorMessageQueues) + { + QList::iterator it = rotatorMessageQueues->begin(); + + for (; it != rotatorMessageQueues->end(); ++it) + { + SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation(); + swgTarget->setName(new QString(m_settings.m_target)); + swgTarget->setAzimuth(azimuth); + swgTarget->setElevation(elevation); + (*it)->push(MainCore::MsgTargetAzimuthElevation::create(m_satelliteTracker, swgTarget)); + } + } + } + + // Send to Map + if (m_settings.m_drawOnMap) + { + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_satelliteTracker, "mapitems"); + if (mapMessageQueues) + { + QString image; + if (sat->m_name == "ISS") + image = "qrc:///satellitetracker/satellitetracker/iss-32.png"; + else + image = "qrc:///satellitetracker/satellitetracker/satellite-32.png"; + + QString text = QString("Name: %1\nAltitude: %2 km\nRange: %3 km\nRange rate: %4 km/s\nSpeed: %5 km/h\nPeriod: %6 mins") + .arg(sat->m_name) + .arg((int)round(satWorkerState->m_satState.m_altitude)) + .arg((int)round(satWorkerState->m_satState.m_range)) + .arg(satWorkerState->m_satState.m_rangeRate, 0, 'f', 1) + .arg(Units::kmpsToIntegerKPH(satWorkerState->m_satState.m_speed)) + .arg((int)round(satWorkerState->m_satState.m_period)); + if (satWorkerState->m_satState.m_passes.size() > 0) + { + if ((qdt >= satWorkerState->m_satState.m_passes[0]->m_aos) && (qdt <= satWorkerState->m_satState.m_passes[0]->m_los)) + text = text.append("\nSatellite is visible"); + else + text = text.append("\nAOS in: %1 mins").arg((int)round((satWorkerState->m_satState.m_passes[0]->m_aos.toSecsSinceEpoch() - qdt.toSecsSinceEpoch())/60.0)); + text = QString("%1\nAOS: %2\nLOS: %3\nMax El: %4%5") + .arg(text) + .arg(satWorkerState->m_satState.m_passes[0]->m_aos.toString(m_settings.m_dateFormat + " hh:mm")) + .arg(satWorkerState->m_satState.m_passes[0]->m_los.toString(m_settings.m_dateFormat + " hh:mm")) + .arg((int)round(satWorkerState->m_satState.m_passes[0]->m_maxElevation)) + .arg(QChar(0xb0)); + } + + sendToMap(mapMessageQueues, sat->m_name, image, text, + satWorkerState->m_satState.m_latitude, satWorkerState->m_satState.m_longitude, + satWorkerState->m_satState.m_altitude * 1000.0, 0, + &satWorkerState->m_satState.m_groundTrack, &satWorkerState->m_satState.m_predictedGroundTrack); + } + } + + // Send to GUI + if (getMessageQueueToGUI()) + getMessageQueueToGUI()->push(SatelliteTrackerReport::MsgReportSat::create(new SatelliteState(satWorkerState->m_satState))); + } + else + qDebug() << "SatelliteTrackerWorker::update: No TLE for " << sat->m_name << ". Can't compute position."; + } + } + m_recalculatePasses = false; +} + +void SatelliteTrackerWorker::aos(SatWorkerState *satWorkerState) +{ + qDebug() << "SatelliteTrackerWorker::aos " << satWorkerState->m_name; + + // Indicate AOS to GUI + if (getMessageQueueToGUI()) + { + int durationMins = (int)round((satWorkerState->m_los.toSecsSinceEpoch() - satWorkerState->m_aos.toSecsSinceEpoch())/60.0); + int maxElevation = 0; + if (satWorkerState->m_satState.m_passes.size() > 0) + maxElevation = satWorkerState->m_satState.m_passes[0]->m_maxElevation; + getMessageQueueToGUI()->push(SatelliteTrackerReport::MsgReportAOS::create(satWorkerState->m_name, durationMins, maxElevation)); + } + + // Update target + if (m_settings.m_autoTarget && (satWorkerState->m_name != m_settings.m_target)) + { + // Only switch if higher priority (earlier in list) or other target not in AOS + SatWorkerState *targetSatWorkerState = m_workerState.value(m_settings.m_target); + int currentTargetIdx = m_settings.m_satellites.indexOf(m_settings.m_target); + int newTargetIdx = m_settings.m_satellites.indexOf(satWorkerState->m_name); + if ((newTargetIdx < currentTargetIdx) || !targetSatWorkerState->hasAOS()) + { + // Stop doppler correction for current target + if (m_workerState.contains(m_settings.m_target)) + m_workerState.value(m_settings.m_target)->m_dopplerTimer.stop(); + + qDebug() << "SatelliteTrackerWorker::aos - autoTarget setting " << satWorkerState->m_name; + m_settings.m_target = satWorkerState->m_name; + // Update GUI with new target + if (getMessageQueueToGUI()) + getMessageQueueToGUI()->push(SatelliteTrackerReport::MsgReportTarget::create(satWorkerState->m_name)); + } + } + + // TODO: Detect if different device sets are used and support multiple sats simultaneously + if (m_settings.m_target == satWorkerState->m_name) + applyDeviceAOSSettings(satWorkerState->m_name); +} + +// Determine if we need to flip rotator or use extended azimuth to avoid 360/0 discontinuity +void SatelliteTrackerWorker::calculateRotation(SatWorkerState *satWorkerState) +{ + m_flipRotation = false; + m_extendedAzRotation = false; + if (satWorkerState->m_satState.m_passes.size() > 0) + { + SatNogsSatellite *sat = m_satellites.value(satWorkerState->m_name); + bool passes0 = getPassesThrough0Deg(sat->m_tle->m_tle0, sat->m_tle->m_tle1, sat->m_tle->m_tle2, + m_settings.m_latitude, m_settings.m_longitude, m_settings.m_heightAboveSeaLevel/1000.0, + satWorkerState->m_satState.m_passes[0]->m_aos, satWorkerState->m_satState.m_passes[0]->m_los); + if (passes0) + { + double aosAz = satWorkerState->m_satState.m_passes[0]->m_aosAzimuth; + double losAz = satWorkerState->m_satState.m_passes[0]->m_losAzimuth; + double minAz = std::min(aosAz, losAz); + if ((m_settings.m_rotatorMaxAzimuth - 360.0) > minAz) + m_extendedAzRotation = true; + else if (m_settings.m_rotatorMaxElevation == 180.0) + m_flipRotation = true; + } + } +} + +void SatelliteTrackerWorker::applyDeviceAOSSettings(const QString& name) +{ + // Execute global program/script + if (!m_settings.m_aosCommand.isEmpty()) + { + qDebug() << "SatelliteTrackerWorker::aos: executing command: " << m_settings.m_aosCommand; + QProcess::startDetached(m_settings.m_aosCommand); + } + + // Update device set + if (m_settings.m_deviceSettings.contains(name)) + { + QList *m_deviceSettingsList = m_settings.m_deviceSettings.value(name); + + MainCore *mainCore = MainCore::instance(); + + // Load presets + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (!devSettings->m_presetGroup.isEmpty() && !devSettings->m_deviceSet.isEmpty()) + { + const MainSettings& mainSettings = mainCore->getSettings(); + + QString presetType = QString(devSettings->m_deviceSet[0]); + const Preset* preset = mainSettings.getPreset(devSettings->m_presetGroup, devSettings->m_presetFrequency, devSettings->m_presetDescription, presetType); + if (preset != nullptr) + { + qDebug() << "SatelliteTrackerWorker::aos: Loading preset " << preset->getDescription() << " to " << devSettings->m_deviceSet[0]; + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + std::vector& deviceSets = mainCore->getDeviceSets(); + if (deviceSetIndex < deviceSets.size()) + { + MainCore::MsgLoadPreset *msg = MainCore::MsgLoadPreset::create(preset, deviceSetIndex); + mainCore->getMainMessageQueue()->push(msg); + } + else + qWarning() << "SatelliteTrackerWorker::aos: device set " << devSettings->m_deviceSet << " does not exist"; + } + else + qWarning() << "SatelliteTrackerWorker::aos: Unable to get preset: " << devSettings->m_presetGroup << " " << devSettings->m_presetFrequency << " " << devSettings->m_presetDescription; + } + } + + // Wait a little bit for presets to load before performing other steps + QTimer::singleShot(1000, [this, mainCore, name, m_deviceSettingsList]() + { + + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + + // Override frequency + if (devSettings->m_frequency != 0) + { + qDebug() << "SatelliteTrackerWorker::aos: setting frequency to: " << devSettings->m_frequency; + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + ChannelWebAPIUtils::setCenterFrequency(deviceSetIndex, devSettings->m_frequency); + } + + // Execute per satellite program/script + if (!devSettings->m_aosCommand.isEmpty()) + { + qDebug() << "SatelliteTrackerWorker::aos: executing command: " << devSettings->m_aosCommand; + QProcess::startDetached(devSettings->m_aosCommand); + } + + } + + // Start acquisition - Need to use WebAPI, in order for GUI to correctly reflect being started + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (devSettings->m_startOnAOS) + { + qDebug() << "SatelliteTrackerWorker::aos: starting acqusition"; + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + ChannelWebAPIUtils::run(deviceSetIndex); + } + } + + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + + // Start file sinks (See issue #782 - currently must occur after starting acqusition) + if (devSettings->m_startStopFileSink) + { + qDebug() << "SatelliteTrackerWorker::aos: starting file sinks"; + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + ChannelWebAPIUtils::startStopFileSinks(deviceSetIndex, true); + } + } + + // Send AOS message to channels + SatWorkerState *satWorkerState = m_workerState.value(name); + ChannelWebAPIUtils::satelliteAOS(name, satWorkerState->m_satState.m_passes[0]->m_northToSouth); + + // Start Doppler correction, if needed + satWorkerState->m_initFrequencyOffset.clear(); + bool requiresDoppler = false; + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (devSettings->m_doppler.size() > 0) + { + requiresDoppler = true; + for (int j = 0; j < devSettings->m_doppler.size(); j++) + { + int offset; + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + if (ChannelWebAPIUtils::getFrequencyOffset(deviceSetIndex, devSettings->m_doppler[j], offset)) + { + satWorkerState->m_initFrequencyOffset.append(offset); + qDebug() << "SatelliteTrackerWorker::applyDeviceAOSSettings: Initial frequency offset: " << offset; + } + else + { + qDebug() << "SatelliteTrackerWorker::applyDeviceAOSSettings: Failed to get initial frequency offset"; + satWorkerState->m_initFrequencyOffset.append(0); + } + } + } + } + if (requiresDoppler) + { + satWorkerState->m_dopplerTimer.setInterval(m_settings.m_dopplerPeriod); + satWorkerState->m_dopplerTimer.start(); + connect(&satWorkerState->m_dopplerTimer, &QTimer::timeout, [this, satWorkerState]() { + doppler(satWorkerState); + }); + } + }); + } + else + { + // Send AOS message to channels + SatWorkerState *satWorkerState = m_workerState.value(name); + ChannelWebAPIUtils::satelliteAOS(name, satWorkerState->m_satState.m_passes[0]->m_northToSouth); + } + +} + +void SatelliteTrackerWorker::doppler(SatWorkerState *satWorkerState) +{ + qDebug() << "SatelliteTrackerWorker::doppler " << satWorkerState->m_name; + + QList *m_deviceSettingsList = m_settings.m_deviceSettings.value(satWorkerState->m_name); + if (m_deviceSettingsList != nullptr) + { + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (devSettings->m_doppler.size() > 0) + { + // Get center frequency for this device + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + double centerFrequency; + if (ChannelWebAPIUtils::getCenterFrequency(deviceSetIndex, centerFrequency)) + { + // Calculate frequency delta due to Doppler + double c = 299792458.0; + double deltaF = centerFrequency * satWorkerState->m_satState.m_rangeRate * 1000.0 / c; + + for (int j = 0; j < devSettings->m_doppler.size(); j++) + { + // For receive, we subtract, transmit we add + int offset; + if (devSettings->m_deviceSet[0] == "R") + offset = satWorkerState->m_initFrequencyOffset[i] - (int)round(deltaF); + else + offset = satWorkerState->m_initFrequencyOffset[i] + (int)round(deltaF); + + if (!ChannelWebAPIUtils::setFrequencyOffset(deviceSetIndex, devSettings->m_doppler[j], offset)) + qDebug() << "SatelliteTrackerWorker::doppler: Failed to set frequency offset"; + } + } + else + qDebug() << "SatelliteTrackerWorker::doppler: couldn't get centre frequency for " << devSettings->m_deviceSet; + } + } + } +} + +void SatelliteTrackerWorker::los(SatWorkerState *satWorkerState) +{ + qDebug() << "SatelliteTrackerWorker::los " << satWorkerState->m_name; + + // Indicate LOS to GUI + if (getMessageQueueToGUI()) + getMessageQueueToGUI()->push(SatelliteTrackerReport::MsgReportLOS::create(satWorkerState->m_name)); + + // Stop Doppler timer, and set interval to 0, so we don't restart it in start() + satWorkerState->m_dopplerTimer.stop(); + satWorkerState->m_dopplerTimer.setInterval(0); + + if (m_settings.m_target == satWorkerState->m_name) + { + // Execute program/script + if (!m_settings.m_losCommand.isEmpty()) + { + qDebug() << "SatelliteTrackerWorker::los: executing command: " << m_settings.m_losCommand; + QProcess::startDetached(m_settings.m_losCommand); + } + + if (m_settings.m_deviceSettings.contains(satWorkerState->m_name)) + { + QList *m_deviceSettingsList = m_settings.m_deviceSettings.value(satWorkerState->m_name); + + // Stop file sinks + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (devSettings->m_startStopFileSink) + { + qDebug() << "SatelliteTrackerWorker::los: stopping file sinks"; + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + ChannelWebAPIUtils::startStopFileSinks(deviceSetIndex, false); + } + } + + // Send LOS message to channels + ChannelWebAPIUtils::satelliteLOS(satWorkerState->m_name); + + // Stop acquisition + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (devSettings->m_stopOnLOS) + { + int deviceSetIndex = devSettings->m_deviceSet.mid(1).toInt(); + ChannelWebAPIUtils::stop(deviceSetIndex); + } + } + + // Execute per satellite program/script + // Do after stopping acquisition, so files are closed by file sink + for (int i = 0; i < m_deviceSettingsList->size(); i++) + { + SatelliteTrackerSettings::SatelliteDeviceSettings *devSettings = m_deviceSettingsList->at(i); + if (!devSettings->m_losCommand.isEmpty()) + { + qDebug() << "SatelliteTrackerWorker::los: executing command: " << devSettings->m_losCommand; + QProcess::startDetached(devSettings->m_losCommand); + } + } + } + } + + // Is another lower-priority satellite with AOS available to switch to? + if (m_settings.m_autoTarget) + { + for (int i = m_settings.m_satellites.indexOf(m_settings.m_target) + 1; i < m_settings.m_satellites.size(); i++) + { + if (m_workerState.contains(m_settings.m_satellites[i])) + { + SatWorkerState *newSatWorkerState = m_workerState.value(m_settings.m_satellites[i]); + if (newSatWorkerState->hasAOS()) + { + qDebug() << "SatelliteTrackerWorker::los - autoTarget setting " << m_settings.m_satellites[i]; + m_settings.m_target = m_settings.m_satellites[i]; + // Update GUI with new target + if (getMessageQueueToGUI()) + getMessageQueueToGUI()->push(SatelliteTrackerReport::MsgReportTarget::create(m_settings.m_target)); + // Apply device settings + applyDeviceAOSSettings(m_settings.m_target); + break; + } + } + } + } + + m_recalculatePasses = true; +} diff --git a/plugins/feature/satellitetracker/satellitetrackerworker.h b/plugins/feature/satellitetracker/satellitetrackerworker.h new file mode 100644 index 000000000..9009359ba --- /dev/null +++ b/plugins/feature/satellitetracker/satellitetrackerworker.h @@ -0,0 +1,144 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_SATELLITETRACKERWORKER_H_ +#define INCLUDE_FEATURE_SATELLITETRACKERWORKER_H_ + +#include +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" +#include "util/astronomy.h" + +#include "satellitetrackersettings.h" +#include "satellitetrackersgp4.h" +#include "satnogs.h" + +class WebAPIAdapterInterface; +class QTcpServer; +class QTcpSocket; +class SatelliteTracker; +class SatelliteTrackerWorker; +class QDateTime; +class QGeoCoordinate; + +class SatWorkerState +{ +public: + SatWorkerState(QString name) : + m_name(name) + { + m_satState.m_name = name; + } + + bool hasAOS() + { + QDateTime currentTime = QDateTime::currentDateTime(); + return (m_aos <= currentTime) && (m_los > currentTime); + } + +protected: + QString m_name; // Name of the satellite + QDateTime m_aos; // Time of next AOS + QDateTime m_los; // Time of next LOS + QTimer m_aosTimer; + QTimer m_losTimer; + QTimer m_dopplerTimer; + QList m_initFrequencyOffset; + SatelliteState m_satState; + + friend SatelliteTrackerWorker; +}; + +class SatelliteTrackerWorker : public QObject +{ + Q_OBJECT + +public: + class MsgConfigureSatelliteTrackerWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const SatelliteTrackerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureSatelliteTrackerWorker* create(const SatelliteTrackerSettings& settings, bool force) + { + return new MsgConfigureSatelliteTrackerWorker(settings, force); + } + + private: + SatelliteTrackerSettings m_settings; + bool m_force; + + MsgConfigureSatelliteTrackerWorker(const SatelliteTrackerSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + SatelliteTrackerWorker(SatelliteTracker* satelliteTracker, WebAPIAdapterInterface *webAPIAdapterInterface); + ~SatelliteTrackerWorker(); + void reset(); + bool startWork(); + void stopWork(); + bool isRunning() const { return m_running; } + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_msgQueueToGUI = messageQueue; } + +private: + + SatelliteTracker* m_satelliteTracker; + WebAPIAdapterInterface *m_webAPIAdapterInterface; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_msgQueueToFeature; //!< Queue to report channel change to main feature object + MessageQueue *m_msgQueueToGUI; + SatelliteTrackerSettings m_settings; + bool m_running; + QMutex m_mutex; + QTimer m_pollTimer; + QHash m_satellites; + QHash m_workerState; + bool m_recalculatePasses; //!< Recalculate passes as something has changed + bool m_flipRotation; //!< Use 180 elevation to avoid 360/0 degree discontinutiy + bool m_extendedAzRotation; //!< Use 450+ degree azimuth to avoid 360/0 degree discontinuity + + bool handleMessage(const Message& cmd); + void applySettings(const SatelliteTrackerSettings& settings, bool force = false); + MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; } + void removeFromMap(QString id); + void sendToMap(QList *mapMessageQueues, QString id, QString image, QString text, + double lat, double lon, double altitude, double rotation, + QList *track = nullptr, QList *predictedTrack = nullptr); + void applyDeviceAOSSettings(const QString& name); + void startStopSinks(bool start); + void calculateRotation(SatWorkerState *satWorkerState); + +private slots: + void handleInputMessages(); + void update(); + void aos(SatWorkerState *satWorkerState); + void los(SatWorkerState *satWorkerState); + void doppler(SatWorkerState *satWorkerState); +}; + +#endif // INCLUDE_FEATURE_SATELLITETRACKERWORKER_H_ diff --git a/plugins/feature/satellitetracker/satnogs.h b/plugins/feature/satellitetracker/satnogs.h new file mode 100644 index 000000000..3f5baae04 --- /dev/null +++ b/plugins/feature/satellitetracker/satnogs.h @@ -0,0 +1,263 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_SATNOGS_H_ +#define INCLUDE_SATNOGS_H_ + +#include +#include +#include +#include +#include + +struct SatNogsTransmitter { + + int m_noradCatId; // To link to which satellite this is for + QString m_description; // E.g. GMSK9k6 G3RUH AX.25 TLM + bool m_alive; + QString m_type; // "Transmitter", "Trasceiver" or "Transponder" + qint64 m_uplinkLow; + qint64 m_uplinkHigh; + qint64 m_downlinkLow; + qint64 m_downlinkHigh; + QString m_mode; // E.g. "GMSK", "CW", "AFSK", "BPSK", "USB", etc + int m_baud; + QString m_status; // "active", "inactive", "invalid" + QString m_service; // "Amateur", "Earth Exploration", "Maritime", "Meteorological", "Mobile", "Space Research" + + SatNogsTransmitter(const QJsonObject& obj) + { + m_noradCatId = obj["norad_cat_id"].toInt(); + m_description = obj["description"].toString(); + m_alive = obj["alive"].toBool(); + m_type = obj["type"].toString(); + m_uplinkLow = (qint64)obj["uplink_low"].toDouble(); + m_uplinkHigh = (qint64)obj["uplink_high"].toDouble(); + m_downlinkLow = (qint64)obj["downlink_low"].toDouble(); + m_downlinkHigh = (qint64)obj["downlink_high"].toDouble(); + m_mode = obj["mode"].toString(); + m_baud = obj["baud"].toInt(); + m_status = obj["status"].toString(); + m_service = obj["service"].toString(); + } + + static QList createList(QJsonArray array) + { + QList list; + for (int i = 0; i < array.size(); i++) + { + QJsonValue value = array.at(i); + if (value.isObject()) + list.append(new SatNogsTransmitter(value.toObject())); + } + + return list; + } + + static QString getFrequencyText(quint64 frequency) + { + if (frequency > 1000000000) + return QString("%1 GHz").arg(frequency/1000000000.0, 0, ',', 6); + else if (frequency > 1000000) + return QString("%1 MHz").arg(frequency/1000000.0, 0, ',', 3); + else + return QString("%1 kHz").arg(frequency/1000.0, 0, ',', 3); + } + + static QString getFrequencyRangeText(quint64 low, quint64 high) + { + if (high > 1000000000) + return QString("%1-%2 GHz").arg(low/1000000000.0, 0, ',', 6).arg(high/1000000000.0, 0, ',', 6); + else if (high > 1000000) + return QString("%1-%2 MHz").arg(low/1000000.0, 0, ',', 3).arg(high/1000000.0, 0, ',', 3); + else + return QString("%1-%2 kHz").arg(low/1000.0, 0, ',', 3).arg(high/1000.0, 0, ',', 3); + } + +}; + +struct SatNogsTLE { + + int m_noradCatId; // To link to which satellite this is for + QString m_tle0; + QString m_tle1; + QString m_tle2; + QDateTime m_updated; + + SatNogsTLE(const QJsonObject &obj) + { + m_noradCatId = obj["norad_cat_id"].toInt(); + m_tle0 = obj["tle0"].toString(); + m_tle1 = obj["tle1"].toString(); + m_tle2 = obj["tle2"].toString(); + m_updated = QDateTime::fromString(obj["updated"].toString(), Qt::ISODateWithMs); + } + + SatNogsTLE(const QString& tle0, const QString& tle1, const QString& tle2) + { + m_noradCatId = tle2.mid(2, 5).toInt(); + m_tle0 = tle0; + m_tle1 = tle1; + m_tle2 = tle2; + } + + static QList createList(QJsonArray array) + { + QList list; + for (int i = 0; i < array.size(); i++) + { + QJsonValue value = array.at(i); + if (value.isObject()) + list.append(new SatNogsTLE(value.toObject())); + } + + return list; + } + + static QList createList(const QByteArray& array) + { + QList list; + QList lines = array.split('\n'); + for (int i = 0; i < lines.size(); i += 3) + { + if (i + 3 < lines.size()) + { + QString tle0(lines[i]); + QString tle1(lines[i+1]); + QString tle2(lines[i+2]); + list.append(new SatNogsTLE(tle0.trimmed(), tle1.trimmed(), tle2.trimmed())); + } + } + return list; + } +}; + +struct SatNogsSatellite { + + int m_noradCatId; + QString m_name; + QStringList m_names; // Alterantive names - JSON "AO-10\r\nOSCAR-10" + QString m_image; // URL to image of satellie - JSON example: "https://db-satnogs.freetls.fastly.net/media/satellites/sigma.jpg" + QString m_status; // "alive" "re-entered" "dead" "future" or "" for TLE only sats + QDateTime m_decayed; // Date of decay. JSON "2018-05-19T00:00:00Z" + QDateTime m_launched; + QDateTime m_deployed; + QString m_website; + QString m_operator; // "None" or "European Space Agency", + QString m_countries; // "US" or "ES" + + QList m_transmitters; + SatNogsTLE *m_tle; + + SatNogsSatellite(const QJsonObject& obj) + { + m_noradCatId = obj["norad_cat_id"].toInt(); + m_name = obj["name"].toString(); + m_names = obj["names"].toString().split("\r\n"); + if ((m_names.size() == 1) && m_names[0].isEmpty()) + m_names = QStringList(); + m_image = obj["image"].toString(); + m_status = obj["status"].toString(); + if (!obj["decayed"].isNull()) + m_decayed = QDateTime::fromString(obj["decayed"].toString(), Qt::ISODate); + if (!obj["launched"].isNull()) + m_launched = QDateTime::fromString(obj["launched"].toString(), Qt::ISODate); + if (!obj["deployed"].isNull()) + m_deployed = QDateTime::fromString(obj["deployed"].toString(), Qt::ISODate); + m_website = obj["website"].toString(); + m_operator = obj["operator"].toString(); + m_countries = obj["countries"].toString(); + m_tle = nullptr; + } + + SatNogsSatellite(SatNogsTLE *tle) + { + // Extract names from TLE + // tle0 is of the form: + // MOZHAYETS 4 (RS-22) + // GOES 9 [-] + QRegExp re("([A-Za-z0-9\\- ]+)([\\(]([A-Z0-9\\- ]+)[\\)])?"); + if (re.indexIn(tle->m_tle0) != -1) + { + QStringList groups = re.capturedTexts(); + m_name = groups[1].trimmed(); + if ((groups.size() >= 4) && (groups[3] != "-") && !groups[3].isEmpty()) + m_names = QStringList({groups[3].trimmed()}); + m_noradCatId = tle->m_tle2.mid(2, 5).toInt(); + m_tle = tle; + } + } + + QString toString() + { + QStringList list; + list.append(QString("Name: %1").arg(m_name)); + list.append(QString("NORAD ID: %1").arg(m_noradCatId)); + if (m_tle != nullptr) + { + list.append(QString("TLE0: %1").arg(m_tle->m_tle0)); + list.append(QString("TLE1: %1").arg(m_tle->m_tle1)); + list.append(QString("TLE2: %1").arg(m_tle->m_tle2)); + } + for (int i = 0; i < m_transmitters.size(); i++) + { + list.append(QString("Mode: %1 Freq: %2").arg(m_transmitters[i]->m_mode).arg(m_transmitters[i]->m_downlinkLow)); + } + return list.join("\n"); + } + + void addTransmitters(const QList& transmitters) + { + for (int i = 0; i < transmitters.size(); i++) + { + SatNogsTransmitter *tx = transmitters[i]; + if (tx->m_noradCatId == m_noradCatId) + m_transmitters.append(tx); + } + } + + void addTLE(const QList& tles) + { + for (int i = 0; i < tles.size(); i++) + { + SatNogsTLE *tle = tles[i]; + if (tle->m_noradCatId == m_noradCatId) + m_tle = tle; + } + } + + // Create a hash table of satellites from the JSON object + static QHash createHash(QJsonArray array) + { + QHash hash; + + for (int i = 0; i < array.size(); i++) + { + QJsonValue value = array.at(i); + if (value.isObject()) + { + SatNogsSatellite *sat = new SatNogsSatellite(value.toObject()); + hash.insert(sat->m_name, sat); + } + } + return hash; + } + +}; + + +#endif // INCLUDE_SATNOGS_H_ diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index 62a104a21..9f27394a4 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -25,20 +25,24 @@ #include "SWGDeviceSettings.h" #include "SWGChannelSettings.h" #include "SWGDeviceSet.h" +#include "SWGChannelActions.h" +#include "SWGFileSinkActions.h" #include "maincore.h" #include "device/deviceset.h" #include "device/deviceapi.h" +#include "channel/channelutils.h" #include "dsp/devicesamplesource.h" #include "dsp/devicesamplesink.h" #include "dsp/devicesamplemimo.h" #include "webapi/webapiadapterinterface.h" #include "webapi/webapiutils.h" +// Get device center frequency bool ChannelWebAPIUtils::getCenterFrequency(unsigned int deviceIndex, double &frequencyInHz) { SWGSDRangel::SWGDeviceSettings deviceSettingsResponse; - SWGSDRangel::SWGErrorResponse errorResponse; + QString errorResponse; int httpRC; DeviceSet *deviceSet; @@ -52,21 +56,21 @@ bool ChannelWebAPIUtils::getCenterFrequency(unsigned int deviceIndex, double &fr deviceSettingsResponse.setDeviceHwType(new QString(deviceSet->m_deviceAPI->getHardwareId())); deviceSettingsResponse.setDirection(0); DeviceSampleSource *source = deviceSet->m_deviceAPI->getSampleSource(); - httpRC = source->webapiSettingsGet(deviceSettingsResponse, *errorResponse.getMessage()); + httpRC = source->webapiSettingsGet(deviceSettingsResponse, errorResponse); } else if (deviceSet->m_deviceSinkEngine) { deviceSettingsResponse.setDeviceHwType(new QString(deviceSet->m_deviceAPI->getHardwareId())); deviceSettingsResponse.setDirection(1); DeviceSampleSink *sink = deviceSet->m_deviceAPI->getSampleSink(); - httpRC = sink->webapiSettingsGet(deviceSettingsResponse, *errorResponse.getMessage()); + httpRC = sink->webapiSettingsGet(deviceSettingsResponse, errorResponse); } else if (deviceSet->m_deviceMIMOEngine) { deviceSettingsResponse.setDeviceHwType(new QString(deviceSet->m_deviceAPI->getHardwareId())); deviceSettingsResponse.setDirection(2); DeviceSampleMIMO *mimo = deviceSet->m_deviceAPI->getSampleMIMO(); - httpRC = mimo->webapiSettingsGet(deviceSettingsResponse, *errorResponse.getMessage()); + httpRC = mimo->webapiSettingsGet(deviceSettingsResponse, errorResponse); } else { @@ -83,7 +87,7 @@ bool ChannelWebAPIUtils::getCenterFrequency(unsigned int deviceIndex, double &fr if (httpRC/100 != 2) { qWarning("ChannelWebAPIUtils::getCenterFrequency: get device frequency error %d: %s", - httpRC, qPrintable(*errorResponse.getMessage())); + httpRC, qPrintable(errorResponse)); return false; } @@ -91,10 +95,11 @@ bool ChannelWebAPIUtils::getCenterFrequency(unsigned int deviceIndex, double &fr return WebAPIUtils::getSubObjectDouble(*jsonObj, "centerFrequency", frequencyInHz); } +// Set device center frequency bool ChannelWebAPIUtils::setCenterFrequency(unsigned int deviceIndex, double frequencyInHz) { SWGSDRangel::SWGDeviceSettings deviceSettingsResponse; - SWGSDRangel::SWGErrorResponse errorResponse; + QString errorResponse; int httpRC; DeviceSet *deviceSet; @@ -108,21 +113,21 @@ bool ChannelWebAPIUtils::setCenterFrequency(unsigned int deviceIndex, double fre deviceSettingsResponse.setDeviceHwType(new QString(deviceSet->m_deviceAPI->getHardwareId())); deviceSettingsResponse.setDirection(0); DeviceSampleSource *source = deviceSet->m_deviceAPI->getSampleSource(); - httpRC = source->webapiSettingsGet(deviceSettingsResponse, *errorResponse.getMessage()); + httpRC = source->webapiSettingsGet(deviceSettingsResponse, errorResponse); } else if (deviceSet->m_deviceSinkEngine) { deviceSettingsResponse.setDeviceHwType(new QString(deviceSet->m_deviceAPI->getHardwareId())); deviceSettingsResponse.setDirection(1); DeviceSampleSink *sink = deviceSet->m_deviceAPI->getSampleSink(); - httpRC = sink->webapiSettingsGet(deviceSettingsResponse, *errorResponse.getMessage()); + httpRC = sink->webapiSettingsGet(deviceSettingsResponse, errorResponse); } else if (deviceSet->m_deviceMIMOEngine) { deviceSettingsResponse.setDeviceHwType(new QString(deviceSet->m_deviceAPI->getHardwareId())); deviceSettingsResponse.setDirection(2); DeviceSampleMIMO *mimo = deviceSet->m_deviceAPI->getSampleMIMO(); - httpRC = mimo->webapiSettingsGet(deviceSettingsResponse, *errorResponse.getMessage()); + httpRC = mimo->webapiSettingsGet(deviceSettingsResponse, errorResponse); } else { @@ -139,7 +144,7 @@ bool ChannelWebAPIUtils::setCenterFrequency(unsigned int deviceIndex, double fre if (httpRC/100 != 2) { qWarning("ChannelWebAPIUtils::setCenterFrequency: get device frequency error %d: %s", - httpRC, qPrintable(*errorResponse.getMessage())); + httpRC, qPrintable(errorResponse)); return false; } @@ -178,3 +183,282 @@ bool ChannelWebAPIUtils::setCenterFrequency(unsigned int deviceIndex, double fre return true; } + +// Start acquisition +bool ChannelWebAPIUtils::run(unsigned int deviceIndex, int subsystemIndex) +{ + SWGSDRangel::SWGDeviceState runResponse; + QString errorResponse; + int httpRC; + DeviceSet *deviceSet; + + std::vector deviceSets = MainCore::instance()->getDeviceSets(); + if (deviceIndex < deviceSets.size()) + { + runResponse.setState(new QString()); + deviceSet = deviceSets[deviceIndex]; + if (deviceSet->m_deviceSourceEngine) + { + DeviceSampleSource *source = deviceSet->m_deviceAPI->getSampleSource(); + httpRC = source->webapiRun(1, runResponse, errorResponse); + } + else if (deviceSet->m_deviceSinkEngine) + { + DeviceSampleSink *sink = deviceSet->m_deviceAPI->getSampleSink(); + httpRC = sink->webapiRun(1, runResponse, errorResponse); + } + else if (deviceSet->m_deviceMIMOEngine) + { + DeviceSampleMIMO *mimo = deviceSet->m_deviceAPI->getSampleMIMO(); + httpRC = mimo->webapiRun(1, subsystemIndex, runResponse, errorResponse); + } + else + { + qDebug() << "ChannelWebAPIUtils::run - unknown device " << deviceIndex; + return false; + } + } + else + { + qDebug() << "ChannelWebAPIUtils::run - no device " << deviceIndex; + return false; + } + + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::run: run error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + return true; +} + +// Stop acquisition +bool ChannelWebAPIUtils::stop(unsigned int deviceIndex, int subsystemIndex) +{ + SWGSDRangel::SWGDeviceState runResponse; + QString errorResponse; + int httpRC; + DeviceSet *deviceSet; + + std::vector deviceSets = MainCore::instance()->getDeviceSets(); + if (deviceIndex < deviceSets.size()) + { + runResponse.setState(new QString()); + deviceSet = deviceSets[deviceIndex]; + if (deviceSet->m_deviceSourceEngine) + { + DeviceSampleSource *source = deviceSet->m_deviceAPI->getSampleSource(); + httpRC = source->webapiRun(0, runResponse, errorResponse); + } + else if (deviceSet->m_deviceSinkEngine) + { + DeviceSampleSink *sink = deviceSet->m_deviceAPI->getSampleSink(); + httpRC = sink->webapiRun(0, runResponse, errorResponse); + } + else if (deviceSet->m_deviceMIMOEngine) + { + DeviceSampleMIMO *mimo = deviceSet->m_deviceAPI->getSampleMIMO(); + httpRC = mimo->webapiRun(0, subsystemIndex, runResponse, errorResponse); + } + else + { + qDebug() << "ChannelWebAPIUtils::stop - unknown device " << deviceIndex; + return false; + } + } + else + { + qDebug() << "ChannelWebAPIUtils::stop - no device " << deviceIndex; + return false; + } + + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::stop: run error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + return true; +} + +// Get input frequency offset for a channel +bool ChannelWebAPIUtils::getFrequencyOffset(unsigned int deviceIndex, int channelIndex, int& offset) +{ + SWGSDRangel::SWGChannelSettings channelSettingsResponse; + QString errorResponse; + int httpRC; + QJsonObject *jsonObj; + double offsetD; + + ChannelAPI *channel = MainCore::instance()->getChannel(deviceIndex, channelIndex); + if (channel != nullptr) + { + httpRC = channel->webapiSettingsGet(channelSettingsResponse, errorResponse); + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::getFrequencyOffset: get channel settings error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + jsonObj = channelSettingsResponse.asJsonObject(); + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "inputFrequencyOffset", offsetD)) + { + offset = (int)offsetD; + return true; + } + } + return false; +} + +// Set input frequency offset for a channel +bool ChannelWebAPIUtils::setFrequencyOffset(unsigned int deviceIndex, int channelIndex, int offset) +{ + SWGSDRangel::SWGChannelSettings channelSettingsResponse; + QString errorResponse; + int httpRC; + QJsonObject *jsonObj; + + ChannelAPI *channel = MainCore::instance()->getChannel(deviceIndex, channelIndex); + if (channel != nullptr) + { + httpRC = channel->webapiSettingsGet(channelSettingsResponse, errorResponse); + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::setFrequencyOffset: get channel settings error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + jsonObj = channelSettingsResponse.asJsonObject(); + + if (WebAPIUtils::setSubObjectDouble(*jsonObj, "inputFrequencyOffset", (double)offset)) + { + QStringList keys; + keys.append("inputFrequencyOffset"); + channelSettingsResponse.init(); + channelSettingsResponse.fromJsonObject(*jsonObj); + httpRC = channel->webapiSettingsPutPatch(false, keys, channelSettingsResponse, errorResponse); + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::setFrequencyOffset: patch channel settings error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + return true; + } + } + return false; +} + +// Start or stop all file sinks in a given device set +bool ChannelWebAPIUtils::startStopFileSinks(unsigned int deviceIndex, bool start) +{ + MainCore *mainCore = MainCore::instance(); + ChannelAPI *channel; + int channelIndex = 0; + while(nullptr != (channel = mainCore->getChannel(deviceIndex, channelIndex))) + { + if (ChannelUtils::compareChannelURIs(channel->getURI(), "sdrangel.channel.filesink")) + { + QStringList channelActionKeys = {"record"}; + SWGSDRangel::SWGChannelActions channelActions; + SWGSDRangel::SWGFileSinkActions *fileSinkAction = new SWGSDRangel::SWGFileSinkActions(); + QString errorResponse; + int httpRC; + + fileSinkAction->setRecord(start); + channelActions.setFileSinkActions(fileSinkAction); + httpRC = channel->webapiActionsPost(channelActionKeys, channelActions, errorResponse); + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::startStopFileSinks: webapiActionsPost error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + } + channelIndex++; + } + return true; +} + +// Send AOS actions to all channels that support it +bool ChannelWebAPIUtils::satelliteAOS(const QString name, bool northToSouthPass) +{ + MainCore *mainCore = MainCore::instance(); + std::vector deviceSets = mainCore->getDeviceSets(); + for (unsigned int deviceIndex = 0; deviceIndex < deviceSets.size(); deviceIndex++) + { + ChannelAPI *channel; + int channelIndex = 0; + while(nullptr != (channel = mainCore->getChannel(deviceIndex, channelIndex))) + { + if (ChannelUtils::compareChannelURIs(channel->getURI(), "sdrangel.channel.aptdemod")) + { + QStringList channelActionKeys = {"aos"}; + SWGSDRangel::SWGChannelActions channelActions; + SWGSDRangel::SWGAPTDemodActions *aptDemodAction = new SWGSDRangel::SWGAPTDemodActions(); + SWGSDRangel::SWGAPTDemodActions_aos *aosAction = new SWGSDRangel::SWGAPTDemodActions_aos(); + QString errorResponse; + int httpRC; + + aosAction->setSatelliteName(new QString(name)); + aosAction->setNorthToSouthPass(northToSouthPass); + aptDemodAction->setAos(aosAction); + + channelActions.setAptDemodActions(aptDemodAction); + httpRC = channel->webapiActionsPost(channelActionKeys, channelActions, errorResponse); + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::satelliteAOS: webapiActionsPost error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + } + channelIndex++; + } + } + return true; +} + +// Send LOS actions to all channels that support it +bool ChannelWebAPIUtils::satelliteLOS(const QString name) +{ + MainCore *mainCore = MainCore::instance(); + std::vector deviceSets = mainCore->getDeviceSets(); + for (unsigned int deviceIndex = 0; deviceIndex < deviceSets.size(); deviceIndex++) + { + ChannelAPI *channel; + int channelIndex = 0; + while(nullptr != (channel = mainCore->getChannel(deviceIndex, channelIndex))) + { + if (ChannelUtils::compareChannelURIs(channel->getURI(), "sdrangel.channel.aptdemod")) + { + QStringList channelActionKeys = {"los"}; + SWGSDRangel::SWGChannelActions channelActions; + SWGSDRangel::SWGAPTDemodActions *aptDemodAction = new SWGSDRangel::SWGAPTDemodActions(); + SWGSDRangel::SWGAPTDemodActions_los *losAction = new SWGSDRangel::SWGAPTDemodActions_los(); + QString errorResponse; + int httpRC; + + losAction->setSatelliteName(new QString(name)); + aptDemodAction->setLos(losAction); + + channelActions.setAptDemodActions(aptDemodAction); + httpRC = channel->webapiActionsPost(channelActionKeys, channelActions, errorResponse); + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::satelliteLOS: webapiActionsPost error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + } + channelIndex++; + } + } + return true; +} diff --git a/sdrbase/channel/channelwebapiutils.h b/sdrbase/channel/channelwebapiutils.h index 624cf6ae6..0550ed34e 100644 --- a/sdrbase/channel/channelwebapiutils.h +++ b/sdrbase/channel/channelwebapiutils.h @@ -18,6 +18,8 @@ #ifndef SDRBASE_CHANNEL_CHANNELWEBAPIUTILS_H_ #define SDRBASE_CHANNEL_CHANNELWEBAPIUTILS_H_ +#include + #include "export.h" class SDRBASE_API ChannelWebAPIUtils @@ -25,6 +27,13 @@ class SDRBASE_API ChannelWebAPIUtils public: static bool getCenterFrequency(unsigned int deviceIndex, double &frequencyInHz); static bool setCenterFrequency(unsigned int deviceIndex, double frequencyInHz); + static bool run(unsigned int deviceIndex, int subsystemIndex=0); + static bool stop(unsigned int deviceIndex, int subsystemIndex=0); + static bool getFrequencyOffset(unsigned int deviceIndex, int channelIndex, int& offset); + static bool setFrequencyOffset(unsigned int deviceIndex, int channelIndex, int offset); + static bool startStopFileSinks(unsigned int deviceIndex, bool start); + static bool satelliteAOS(const QString name, bool northToSouthPass); + static bool satelliteLOS(const QString name); }; #endif // SDRBASE_CHANNEL_CHANNELWEBAPIUTILS_H_ diff --git a/sdrbase/plugin/pluginmanager.cpp b/sdrbase/plugin/pluginmanager.cpp index 96cc410dc..1e327f606 100644 --- a/sdrbase/plugin/pluginmanager.cpp +++ b/sdrbase/plugin/pluginmanager.cpp @@ -338,3 +338,28 @@ const PluginInterface *PluginManager::getFeaturePluginInterface(const QString& f return nullptr; } + +QString PluginManager::uriToId(const QString& uri) const +{ + for (int i = 0; i < m_rxChannelRegistrations.size(); i++) + { + if (m_rxChannelRegistrations[i].m_channelIdURI == uri) + return m_rxChannelRegistrations[i].m_channelId; + } + for (int i = 0; i < m_txChannelRegistrations.size(); i++) + { + if (m_txChannelRegistrations[i].m_channelIdURI == uri) + return m_txChannelRegistrations[i].m_channelId; + } + for (int i = 0; i < m_mimoChannelRegistrations.size(); i++) + { + if (m_mimoChannelRegistrations[i].m_channelIdURI == uri) + return m_mimoChannelRegistrations[i].m_channelId; + } + for (int i = 0; i < m_featureRegistrations.size(); i++) + { + if (m_featureRegistrations[i].m_featureIdURI == uri) + return m_featureRegistrations[i].m_featureId; + } + return uri; +} diff --git a/sdrbase/plugin/pluginmanager.h b/sdrbase/plugin/pluginmanager.h index 705c38d5f..71713810c 100644 --- a/sdrbase/plugin/pluginmanager.h +++ b/sdrbase/plugin/pluginmanager.h @@ -89,6 +89,9 @@ public: const PluginInterface *getDevicePluginInterface(const QString& deviceId) const; const PluginInterface *getFeaturePluginInterface(const QString& featureIdURI) const; + // Map channel/feature URI to short form Id + QString uriToId(const QString& uri) const; + static const QString& getFileInputDeviceId() { return m_fileInputDeviceTypeID; } static const QString& getTestMIMODeviceId() { return m_testMIMODeviceTypeID; } static const QString& getFileOutputDeviceId() { return m_fileOutputDeviceTypeID; } diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h index 877da1fe9..3f79f2dac 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -61,6 +61,16 @@ public: return (int)std::round(knotsToKPH(knots)); } + static float kmpsToKPH(float kps) + { + return kps * (60.0 * 60.0); + } + + static int kmpsToIntegerKPH(float kps) + { + return (int)std::round(kmpsToKPH(kps)); + } + static float feetPerMinToMetresPerSecond(float fpm) { return fpm * 0.00508f; diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index bc5d11e86..5a89b4b31 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -55,6 +55,7 @@ set(sdrgui_SOURCES gui/samplingdevicedialog.cpp gui/samplingdevicesdock.cpp gui/scaleengine.cpp + gui/scaledimage.cpp gui/sdrangelsplash.cpp gui/tickedslider.cpp gui/transverterbutton.cpp @@ -64,6 +65,7 @@ set(sdrgui_SOURCES gui/valuedial.cpp gui/valuedialz.cpp gui/wsspectrumsettingsdialog.cpp + gui/wrappingdatetimeedit.cpp dsp/scopevis.cpp dsp/scopevisxy.cpp @@ -141,6 +143,7 @@ set(sdrgui_HEADERS gui/samplingdevicedialog.h gui/samplingdevicesdock.h gui/scaleengine.h + gui/scaledimage.h gui/sdrangelsplash.h gui/tickedslider.h gui/transverterbutton.h @@ -150,6 +153,7 @@ set(sdrgui_HEADERS gui/valuedial.h gui/valuedialz.h gui/wsspectrumsettingsdialog.h + gui/wrappingdatetimeedit.h dsp/scopevis.h dsp/scopevisxy.h diff --git a/sdrgui/gui/scaledimage.cpp b/sdrgui/gui/scaledimage.cpp new file mode 100644 index 000000000..365c61d39 --- /dev/null +++ b/sdrgui/gui/scaledimage.cpp @@ -0,0 +1,41 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 "scaledimage.h" + +ScaledImage::ScaledImage(QWidget *parent) : + QLabel(parent) +{ +} + +void ScaledImage::setPixmap(const QPixmap& pixmap) +{ + setPixmap(pixmap, size()); +} + +void ScaledImage::setPixmap(const QPixmap& pixmap, const QSize& size) +{ + m_pixmap = pixmap; + m_pixmapScaled = pixmap.scaled(size, Qt::KeepAspectRatio); + QLabel::setPixmap(m_pixmapScaled); +} + +void ScaledImage::resizeEvent(QResizeEvent *event) +{ + QLabel::resizeEvent(event); + setPixmap(m_pixmap, event->size()); +} diff --git a/sdrgui/gui/scaledimage.h b/sdrgui/gui/scaledimage.h new file mode 100644 index 000000000..b8e2a5724 --- /dev/null +++ b/sdrgui/gui/scaledimage.h @@ -0,0 +1,47 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 SDRGUI_GUI_SCALEDIMAGE_H +#define SDRGUI_GUI_SCALEDIMAGE_H + +#include +#include +#include +#include + +#include "export.h" + +// Similar to displaying a pixmap with QLabel, except we preserve the aspect ratio +class SDRGUI_API ScaledImage : public QLabel { + +public: + explicit ScaledImage(QWidget *parent = nullptr); + + void setPixmap(const QPixmap& pixmap); + void setPixmap(const QPixmap& pixmap, const QSize& size); + +protected: + + virtual void resizeEvent(QResizeEvent *event); + +private: + QPixmap m_pixmap; + QPixmap m_pixmapScaled; + +}; + +#endif // SDRGUI_GUI_SCALEDIMAGE_H diff --git a/sdrgui/gui/wrappingdatetimeedit.cpp b/sdrgui/gui/wrappingdatetimeedit.cpp new file mode 100644 index 000000000..14cac8347 --- /dev/null +++ b/sdrgui/gui/wrappingdatetimeedit.cpp @@ -0,0 +1,71 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 "wrappingdatetimeedit.h" + +WrappingDateTimeEdit::WrappingDateTimeEdit(QWidget *parent) : + QDateTimeEdit(parent) +{ + setWrapping(true); +} + +void WrappingDateTimeEdit::stepBy(int steps) +{ + if (currentSection() == QDateTimeEdit::MonthSection) + setDate(date().addMonths(steps)); + else if (currentSection() == QDateTimeEdit::DaySection) + setDate(date().addDays(steps)); + else if (currentSection() == QDateTimeEdit::HourSection) + { + QTime t = time(); + int h = t.hour(); + + setTime(time().addSecs(steps*3600)); + + if ((h < -steps) && (steps < 0)) + setDate(date().addDays(-1)); + else if ((h + steps > 23) && (steps > 0)) + setDate(date().addDays(1)); + } + else if (currentSection() == QDateTimeEdit::MinuteSection) + { + QTime t = time(); + int h = t.hour(); + int m = t.minute(); + + setTime(time().addSecs(steps*60)); + + if ((m < -steps) && (steps < 0) && (h == 0)) + setDate(date().addDays(-1)); + else if ((m + steps > 59) && (steps > 0) && (h == 23)) + setDate(date().addDays(1)); + } + else if (currentSection() == QDateTimeEdit::SecondSection) + { + QTime t = time(); + int h = t.hour(); + int m = t.minute(); + int s = t.second(); + + setTime(time().addSecs(steps)); + + if ((s < -steps) && (steps < 0) && (h == 0) && (m == 0)) + setDate(date().addDays(-1)); + else if ((s + steps > 59) && (steps > 0) && (h == 23) && (m == 59)) + setDate(date().addDays(1)); + } +} diff --git a/sdrgui/gui/wrappingdatetimeedit.h b/sdrgui/gui/wrappingdatetimeedit.h new file mode 100644 index 000000000..ddc766222 --- /dev/null +++ b/sdrgui/gui/wrappingdatetimeedit.h @@ -0,0 +1,35 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 SDRGUI_GUI_WRAPPINGDATETIMEEDIT_H +#define SDRGUI_GUI_WRAPPINGDATETIMEEDIT_H + +#include + +#include "export.h" + +// Same as QDateTimeEdit, except allows minutes to wrap to hours and hours to +// days when scrolling up or down +class SDRGUI_API WrappingDateTimeEdit : public QDateTimeEdit { + +public: + explicit WrappingDateTimeEdit(QWidget *parent = nullptr); + + void stepBy(int steps) override; +}; + +#endif // SDRGUI_GUI_WRAPPINGDATETIMEEDIT_H diff --git a/sdrgui/resources/arrow_right.png b/sdrgui/resources/arrow_right.png new file mode 100644 index 000000000..0ac47450d Binary files /dev/null and b/sdrgui/resources/arrow_right.png differ diff --git a/sdrgui/resources/res.qrc b/sdrgui/resources/res.qrc index 5e54933c6..44b9a16c4 100644 --- a/sdrgui/resources/res.qrc +++ b/sdrgui/resources/res.qrc @@ -2,6 +2,7 @@ info.png arrow_left.png + arrow_right.png star.png swap.png gridpolar.png