From 4d04ee1c31c84c0746c879b695ef192982570923 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Wed, 13 Jan 2021 20:51:38 +0000 Subject: [PATCH] Add StarTracker feature --- plugins/feature/CMakeLists.txt | 2 + plugins/feature/startracker/CMakeLists.txt | 61 ++ plugins/feature/startracker/readme.md | 158 +++ plugins/feature/startracker/startracker.cpp | 502 ++++++++++ plugins/feature/startracker/startracker.h | 139 +++ plugins/feature/startracker/startracker.qrc | 17 + .../startracker/startracker/moon-32.png | Bin 0 -> 1740 bytes .../startracker/moon-first-quarter-32.png | Bin 0 -> 1367 bytes .../startracker/startracker/moon-flat-32.png | Bin 0 -> 1042 bytes .../startracker/startracker/moon-full-32.png | Bin 0 -> 1740 bytes .../startracker/startracker/moon-new-32.png | Bin 0 -> 607 bytes .../startracker/startracker/moon-old-32.png | Bin 0 -> 1354 bytes .../startracker/moon-third-quarter-32.png | Bin 0 -> 1536 bytes .../startracker/moon-waning-crescent-32.png | Bin 0 -> 1354 bytes .../startracker/moon-waning-gibbous-32.png | Bin 0 -> 1986 bytes .../startracker/moon-waxing-crescent-32.png | Bin 0 -> 1313 bytes .../startracker/moon-waxing-gibbous-32.png | Bin 0 -> 1824 bytes .../startracker/startracker/moon-young-32.png | Bin 0 -> 721 bytes .../startracker/startracker/pulsar-32.png | Bin 0 -> 432 bytes .../startracker/startracker/sun-32.png | Bin 0 -> 713 bytes .../startracker/startracker/sun-40.png | Bin 0 -> 1170 bytes .../feature/startracker/startrackergui.cpp | 582 ++++++++++++ plugins/feature/startracker/startrackergui.h | 100 ++ plugins/feature/startracker/startrackergui.ui | 503 ++++++++++ .../feature/startracker/startrackerplugin.cpp | 80 ++ .../feature/startracker/startrackerplugin.h | 49 + .../feature/startracker/startrackerreport.h | 83 ++ .../startracker/startrackersettings.cpp | 166 ++++ .../feature/startracker/startrackersettings.h | 67 ++ .../startracker/startrackersettingsdialog.cpp | 68 ++ .../startracker/startrackersettingsdialog.h | 40 + .../startracker/startrackersettingsdialog.ui | 382 ++++++++ .../startracker/startrackerwebapiadapter.cpp | 52 + .../startracker/startrackerwebapiadapter.h | 50 + .../feature/startracker/startrackerworker.cpp | 551 +++++++++++ .../feature/startracker/startrackerworker.h | 109 +++ sdrbase/CMakeLists.txt | 4 + sdrbase/util/astronomy.cpp | 896 ++++++++++++++++++ sdrbase/util/astronomy.h | 68 ++ 39 files changed, 4729 insertions(+) create mode 100644 plugins/feature/startracker/CMakeLists.txt create mode 100644 plugins/feature/startracker/readme.md create mode 100644 plugins/feature/startracker/startracker.cpp create mode 100644 plugins/feature/startracker/startracker.h create mode 100644 plugins/feature/startracker/startracker.qrc create mode 100644 plugins/feature/startracker/startracker/moon-32.png create mode 100644 plugins/feature/startracker/startracker/moon-first-quarter-32.png create mode 100644 plugins/feature/startracker/startracker/moon-flat-32.png create mode 100644 plugins/feature/startracker/startracker/moon-full-32.png create mode 100644 plugins/feature/startracker/startracker/moon-new-32.png create mode 100644 plugins/feature/startracker/startracker/moon-old-32.png create mode 100644 plugins/feature/startracker/startracker/moon-third-quarter-32.png create mode 100644 plugins/feature/startracker/startracker/moon-waning-crescent-32.png create mode 100644 plugins/feature/startracker/startracker/moon-waning-gibbous-32.png create mode 100644 plugins/feature/startracker/startracker/moon-waxing-crescent-32.png create mode 100644 plugins/feature/startracker/startracker/moon-waxing-gibbous-32.png create mode 100644 plugins/feature/startracker/startracker/moon-young-32.png create mode 100644 plugins/feature/startracker/startracker/pulsar-32.png create mode 100644 plugins/feature/startracker/startracker/sun-32.png create mode 100644 plugins/feature/startracker/startracker/sun-40.png create mode 100644 plugins/feature/startracker/startrackergui.cpp create mode 100644 plugins/feature/startracker/startrackergui.h create mode 100644 plugins/feature/startracker/startrackergui.ui create mode 100644 plugins/feature/startracker/startrackerplugin.cpp create mode 100644 plugins/feature/startracker/startrackerplugin.h create mode 100644 plugins/feature/startracker/startrackerreport.h create mode 100644 plugins/feature/startracker/startrackersettings.cpp create mode 100644 plugins/feature/startracker/startrackersettings.h create mode 100644 plugins/feature/startracker/startrackersettingsdialog.cpp create mode 100644 plugins/feature/startracker/startrackersettingsdialog.h create mode 100644 plugins/feature/startracker/startrackersettingsdialog.ui create mode 100644 plugins/feature/startracker/startrackerwebapiadapter.cpp create mode 100644 plugins/feature/startracker/startrackerwebapiadapter.h create mode 100644 plugins/feature/startracker/startrackerworker.cpp create mode 100644 plugins/feature/startracker/startrackerworker.h create mode 100644 sdrbase/util/astronomy.cpp create mode 100644 sdrbase/util/astronomy.h diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index bb427681a..0e1e7d3da 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -10,6 +10,8 @@ if (Qt5Quick_FOUND AND Qt5QuickWidgets_FOUND AND Qt5Positioning_FOUND) endif() add_subdirectory(afc) +add_subdirectory(aprs) add_subdirectory(demodanalyzer) add_subdirectory(rigctlserver) add_subdirectory(simpleptt) +add_subdirectory(startracker) diff --git a/plugins/feature/startracker/CMakeLists.txt b/plugins/feature/startracker/CMakeLists.txt new file mode 100644 index 000000000..8455f0819 --- /dev/null +++ b/plugins/feature/startracker/CMakeLists.txt @@ -0,0 +1,61 @@ +project(startracker) + +set(startracker_SOURCES + startracker.cpp + startrackersettings.cpp + startrackerplugin.cpp + startrackerworker.cpp + startrackerwebapiadapter.cpp +) + +set(startracker_HEADERS + startracker.h + startrackersettings.h + startrackerplugin.h + startrackerreport.h + startrackerworker.h + startrackerwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(startracker_SOURCES + ${startracker_SOURCES} + startrackergui.cpp + startrackergui.ui + startrackersettingsdialog.cpp + startrackersettingsdialog.ui + startracker.qrc + ) + set(startracker_HEADERS + ${startracker_HEADERS} + startrackergui.h + startrackersettingsdialog.h + ) + + set(TARGET_NAME featurestartracker) + set(TARGET_LIB Qt5::Widgets Qt5::Charts) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featurestartrackersrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${startracker_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/feature/startracker/readme.md b/plugins/feature/startracker/readme.md new file mode 100644 index 000000000..6d2a3c332 --- /dev/null +++ b/plugins/feature/startracker/readme.md @@ -0,0 +1,158 @@ +

Star Tracker Feature Plugin

+ +

Introduction

+ +The Star Tracker feature plugin is for use in radio astronomy and EME (Earth-Moon-Earth) communication. +It calculates the azimuth and elevation of celestial objects and can send them to the Rotator Controller or other plugins to point an antenna at that object. +The overhead position of the Sun, Moon and selected star can be displayed on the Map Feature. +The plugin can communicate with Stellarium, allowing Stellarium to control SDRangel as though it was a telescope and for the direction the antenna is pointing to be displayed in Stellarium. + +

Interface

+ +![Star Tracker feature plugin GUI](../../../doc/img/StarTracker_plugin.png) + +

1: Start/Stop plugin

+ +This button starts or stops the plugin. The plugin will only calculate azimuth and elevation or communicate with Stellarium when started. + +

2: Find target on map

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

3: Set latitude and longitude from My Position/h3> + +When clicked, it sets the latitude, longitude and height fields to the values from SDRangel's My Position preferences. + +

4: Show settings dialog

+ +Pressing this button displays a settings dialog, that allows you to set: + +* The epoch used when entering RA and Dec. This can be either J2000 (which is used for most catalogues) or JNOW which is the current date and time. +* The units used for the display of the calculated azimuth and elevation. This can be either degrees, minutes and seconds or decimal degrees. +* Whether to correct for atmospheric refaction. You can choose either no correction, the Saemundsson algorithm, typically used for optical astronomy or the more accurate Positional Astronomy Library calculation, which can be used for >250MHz radio frequencies or light. Note that there is only a very minor difference between the two. +* Air pressure in millibars for use in refraction correction. +* Air temperature in degrees Celsius for use in refraction correction. +* Relative humidity in % for use in refraction correction. +* Height above sea level in metres for use in refraction correction. +* Temperature lapse rate in Kelvin per kilometer for use in refraction correction. +* Radio frequency being observed in MHz for use in refraction correction. +* The update period in seconds, which controls how frequently azimuth and elevation are re-calculated. +* The IP port number the Stellarium server listens on. +* Whether to start a Stellarium telescope server. +* Whether to draw the Sun in the map. +* Whether to draw the Moon on the map. +* Whether to draw the target star (or galaxy) on the map. + +

5: Latitude/h3> + +Specifies the latitude in decimal degrees of the observation point (antenna location). + +

6: Longitude

+ +Specifies the longitude in decimal degrees of the observation point (antenna location). + +

7: Time

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

8: Target

+ +Select a target object to track from the list. +To manually enter RA (right ascension) and Dec (declination) of an unlisted target, select Custom. +To allow Stellarium to set the RA and Dec, select Custom, and ensure the Stellarium Server option is checked in the Star Tracker Settings dialog. + +| Target | Type | Details | Flux density (Jy) | +|------------------|-------------------|------------------------------------------------|--------------------------------------------- +| Sun | Star | Targets our Sun | 10k-10M (50MHz), 500k-10M (1.4GHz) | +| Moon | Moon | Targets our Moon | 2 (50MHz), 1000 (1.4GHz) | +| PSR B0329+54 | Pulsar | Strongest in Northern hemisphere (J0332+5434) | 1.8 (50MHz), 1.5 (400MHz), 0.2 (1.4GHz) | +| PSR B0833-45 | Pulsar | Strongest in Southern hemisphere (J0835-4510) | 5.4 (150MHz), 5.0 (400MHz), 1.0 (1.4GHz) | +| Sagittarius A | Galatic centre | First detected source of extrasolar radio | ~0.5 (<1GHz) for Sgr A* | +| Cassiopeia A | Supernova | Brightest extrasolar radio source | 27k (50MHz), 10k (150MHz), 1768 (1.4GHz) | +| Cygnus A | Galaxy | First radio galaxy | 22k (50MHz), 11k (150MHz), 1579 (1.4GHz) | +| Taurus A (M1) | Supernova/Pulsar | Crab Nebular | 2008 (50MHz), 1368 (150MHz), 829 (1.4GHz) | +| Virgo A (M87) | Galaxy | | 2635 (50MHz), 1209 (150MHz), 212 (1.4GHz) | +| Custom | | Manually enter RA and Dec | | + +References: + +* ATNF Pulsar Catalogue - https://www.atnf.csiro.au/research/pulsar/psrcat/ +* Cassiopeia A, Cygnus A, Taurus A, and Virgo A at ultra-low radio frequencies - https://research.chalmers.se/publication/516438/file/516438_Fulltext.pdf +* Repeating Jansky - https://www.gb.nrao.edu/~fghigo/JanskyAntenna/RepeatingJansky_memo10.pdf + +

9: Right Ascension

+ +When target is set to Custom, you can specify the right ascension in hours of the target object. This can be specified as a decimal (E.g. 12.23, from 0 to 24) or in hours, minutes and seconds (E.g. 12h05m10.2s or 12 05 10.2). Whether the epoch is J2000 or JNOW can be set in the Star Tracker Settings dialog. + +

10: Declination

+ +When target is set to Custom, you can specify the declination in degrees of the target object. This can be specified as a decimal (E.g. 34.6, from -90.0 to 90.0) or in degrees, minutes and seconds (E.g. 34d12m5.6s, 34d12'5.6" 34 12 5.6). Whether the epoch is J2000 or JNOW can be set in the Star Tracker Settings dialog. + +

11: Azimuth

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

12: Elevation

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

13: Elevation vs Time Plot

+ +In order to assit in determining whether and when observations of the target object may be possible, an elevation vs time plot is drawn for the 24 hours encompassing the selected date and time. +Some objects may not be visible from a particular latitude for the specified time, in which case, the grahp title will indicate the object is not visible on that particular date. + +

Map

+ +The Star Tracker feature can send the overhead position of the Sun, Moon and target Star to the Map. These can be enabled individually in the settings dialog. The Moon should be displayed with an approximate phase. Stars (or galaxies) are displayed as an image of a pulsar. + +When using the Find feature in the Map GUI, you can search for "Sun", "Moon" or "Star". + +

Stellarium Interface

+ +In Star Tracker: + +* Set target to Custom +* Press Show settings dialog and ensure Stellarium server is checked +* Press Start + +Then in Stellarium: + +* Enable Telescope Control plugin and restart +* Press the telescope button in the main toolbar +* Press "Configure telescopes..." +* Press "Add a new telescope" +* Set "Telescope controlled by" to "External softare or a remote computer" +* Set "Name" to "SDRangel" (Optional) +* Set "Coordinate system" to "J2000 (default)" +* Press OK +* Press Connect +* Enter Right Ascension/Declination or press "Current object" to get RA/Dec of currently selected object +* Press "Slew" to send the RA/Dec to Star Tracker + +Star Tracker will continually send the RA/Dec of its target to Stellarium and this should be displayed in Stellarium with a crosshair/reticle and the label SDRangel (or whatever name you entered for the telescope). + +To see the rough field of view of your antenna, open the Ocular configuration window and under Eyepieces, add a new eyepiece with name SDRangel. +Set aFOV to the half-power beam width of your antenna, focal length to 100 and field stop to 0. +Then select the SDRangel telescope reticle and press Ocular view. + +![StarTracker map](../../../doc/img/StarTracker_map.png) + +

Attribution

+ +Icons are by Adnen Kadri and Erik Madsen, from the Noun Project Noun Project: https://thenounproject.com/ + +Icons are by Freepik from Flaticon https://www.flaticon.com/ + +

API

+ +Full details of the API can be found in the Swagger documentation. Here is a quick example of how to set the target to the Moon at the current time: + + curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/settings" -d '{"featureType": "StarTracker", "StarTrackerSettings": { "target": "Moon", "dateTime": "" }}' + +Or to a custom RA and declination on a given date and time: + + curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/settings" -d '{"featureType": "StarTracker", "StarTrackerSettings": { "target": "Custom", "ra": "03h32m59.35s", "dec": "54d34m45.05s", "dateTime": "1921-04-15T10:17:05" }}' + +To start tracking: + + curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/run" diff --git a/plugins/feature/startracker/startracker.cpp b/plugins/feature/startracker/startracker.cpp new file mode 100644 index 000000000..c817c99ae --- /dev/null +++ b/plugins/feature/startracker/startracker.cpp @@ -0,0 +1,502 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "SWGSimplePTTReport.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" + +#include "startrackerworker.h" +#include "startracker.h" + +MESSAGE_CLASS_DEFINITION(StarTracker::MsgConfigureStarTracker, Message) +MESSAGE_CLASS_DEFINITION(StarTracker::MsgStartStop, Message) + +const char* const StarTracker::m_featureIdURI = "sdrangel.feature.startracker"; +const char* const StarTracker::m_featureId = "StarTracker"; + +StarTracker::StarTracker(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_ptt(false) +{ + qDebug("StarTracker::StarTracker: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_worker = new StarTrackerWorker(this, webAPIAdapterInterface); + m_state = StIdle; + m_errorMessage = "StarTracker error"; +} + +StarTracker::~StarTracker() +{ + if (m_worker->isRunning()) { + stop(); + } + + delete m_worker; +} + +void StarTracker::start() +{ + qDebug("StarTracker::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(); + + StarTrackerWorker::MsgConfigureStarTrackerWorker *msg = StarTrackerWorker::MsgConfigureStarTrackerWorker::create(m_settings, true); + m_worker->getInputMessageQueue()->push(msg); +} + +void StarTracker::stop() +{ + qDebug("StarTracker::stop"); + m_worker->stopWork(); + m_state = StIdle; + m_thread.quit(); + m_thread.wait(); +} + +bool StarTracker::handleMessage(const Message& cmd) +{ + if (MsgConfigureStarTracker::match(cmd)) + { + MsgConfigureStarTracker& cfg = (MsgConfigureStarTracker&) cmd; + qDebug() << "StarTracker::handleMessage: MsgConfigureStarTracker"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + qDebug() << "StarTracker::handleMessage: MsgStartStop: start:" << cfg.getStartStop(); + + if (cfg.getStartStop()) { + start(); + } else { + stop(); + } + + return true; + } + else + { + return false; + } +} + +QByteArray StarTracker::serialize() const +{ + return m_settings.serialize(); +} + +bool StarTracker::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureStarTracker *msg = MsgConfigureStarTracker::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureStarTracker *msg = MsgConfigureStarTracker::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void StarTracker::applySettings(const StarTrackerSettings& settings, bool force) +{ + qDebug() << "StarTracker::applySettings:" + << " m_target: " << settings.m_target + << " m_ra: " << settings.m_ra + << " m_dec: " << settings.m_dec + << " m_latitude: " << settings.m_latitude + << " m_longitude: " << settings.m_longitude + << " m_serverPort: " << settings.m_serverPort + << " m_enableServer: " << settings.m_enableServer + << " 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_target != settings.m_target) || force) { + reverseAPIKeys.append("target"); + } + if ((m_settings.m_ra != settings.m_ra) || force) { + reverseAPIKeys.append("ra"); + } + if ((m_settings.m_dec != settings.m_dec) || force) { + reverseAPIKeys.append("dec"); + } + 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_dateTime != settings.m_dateTime) || force) { + reverseAPIKeys.append("dateTime"); + } + if ((m_settings.m_refraction != settings.m_refraction) || force) { + reverseAPIKeys.append("refraction"); + } + if ((m_settings.m_pressure != settings.m_pressure) || force) { + reverseAPIKeys.append("pressure"); + } + if ((m_settings.m_temperature != settings.m_temperature) || force) { + reverseAPIKeys.append("temperature"); + } + if ((m_settings.m_humidity != settings.m_humidity) || force) { + reverseAPIKeys.append("humidity"); + } + if ((m_settings.m_heightAboveSeaLevel != settings.m_heightAboveSeaLevel) || force) { + reverseAPIKeys.append("heightAboveSeaLevel"); + } + if ((m_settings.m_temperatureLapseRate != settings.m_temperatureLapseRate) || force) { + reverseAPIKeys.append("temperatureLapseRate"); + } + if ((m_settings.m_frequency != settings.m_frequency) || force) { + reverseAPIKeys.append("frequency"); + } + if ((m_settings.m_serverPort != settings.m_serverPort) || force) { + reverseAPIKeys.append("stellariumPort"); + } + if ((m_settings.m_enableServer != settings.m_enableServer) || force) { + reverseAPIKeys.append("stellariumServerEnabled"); + } + if ((m_settings.m_updatePeriod != settings.m_updatePeriod) || force) { + reverseAPIKeys.append("updatePeriod"); + } + if ((m_settings.m_jnow != settings.m_jnow) || force) { + reverseAPIKeys.append("epoch"); + } + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + StarTrackerWorker::MsgConfigureStarTrackerWorker *msg = StarTrackerWorker::MsgConfigureStarTrackerWorker::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; +} + +int StarTracker::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + getFeatureStateStr(*response.getState()); + MsgStartStop *msg = MsgStartStop::create(run); + getInputMessageQueue()->push(msg); + return 202; +} + +int StarTracker::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setStarTrackerSettings(new SWGSDRangel::SWGStarTrackerSettings()); + response.getStarTrackerSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int StarTracker::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + StarTrackerSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureStarTracker *msg = MsgConfigureStarTracker::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("StarTracker::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureStarTracker *msgToGUI = MsgConfigureStarTracker::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + return 200; +} + +void StarTracker::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const StarTrackerSettings& settings) +{ + response.getStarTrackerSettings()->setTarget(new QString(settings.m_target)); + response.getStarTrackerSettings()->setRa(new QString(settings.m_ra)); + response.getStarTrackerSettings()->setDec(new QString(settings.m_dec)); + response.getStarTrackerSettings()->setLatitude(settings.m_latitude); + response.getStarTrackerSettings()->setLongitude(settings.m_longitude); + response.getStarTrackerSettings()->setDateTime(new QString(settings.m_dateTime)); + response.getStarTrackerSettings()->setRefraction(new QString(settings.m_refraction)); + response.getStarTrackerSettings()->setPressure(settings.m_pressure); + response.getStarTrackerSettings()->setTemperature(settings.m_temperature); + response.getStarTrackerSettings()->setHumidity(settings.m_humidity); + response.getStarTrackerSettings()->setHeightAboveSeaLevel(settings.m_heightAboveSeaLevel); + response.getStarTrackerSettings()->setTemperatureLapseRate(settings.m_temperatureLapseRate); + response.getStarTrackerSettings()->setFrequency(settings.m_frequency/1000000.0); + response.getStarTrackerSettings()->setStellariumServerEnabled(settings.m_enableServer ? 1 : 0); + response.getStarTrackerSettings()->setStellariumPort(settings.m_serverPort); + response.getStarTrackerSettings()->setUpdatePeriod(settings.m_updatePeriod); + response.getStarTrackerSettings()->setEpoch(settings.m_jnow ? new QString("JNOW") : new QString("J2000")); + + if (response.getStarTrackerSettings()->getTitle()) { + *response.getStarTrackerSettings()->getTitle() = settings.m_title; + } else { + response.getStarTrackerSettings()->setTitle(new QString(settings.m_title)); + } + + response.getStarTrackerSettings()->setRgbColor(settings.m_rgbColor); + response.getStarTrackerSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getStarTrackerSettings()->getReverseApiAddress()) { + *response.getStarTrackerSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getStarTrackerSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getStarTrackerSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getStarTrackerSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex); + response.getStarTrackerSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex); +} + +void StarTracker::webapiUpdateFeatureSettings( + StarTrackerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("target")) { + settings.m_target = *response.getStarTrackerSettings()->getTarget(); + } + if (featureSettingsKeys.contains("ra")) { + settings.m_ra = *response.getStarTrackerSettings()->getRa(); + } + if (featureSettingsKeys.contains("dec")) { + settings.m_dec = *response.getStarTrackerSettings()->getDec(); + } + if (featureSettingsKeys.contains("latitude")) { + settings.m_latitude = response.getStarTrackerSettings()->getLatitude(); + } + if (featureSettingsKeys.contains("longitude")) { + settings.m_longitude = response.getStarTrackerSettings()->getLongitude(); + } + if (featureSettingsKeys.contains("dateTime")) { + settings.m_dateTime = *response.getStarTrackerSettings()->getDateTime(); + } + if (featureSettingsKeys.contains("pressure")) { + settings.m_pressure = response.getStarTrackerSettings()->getPressure(); + } + if (featureSettingsKeys.contains("temperature")) { + settings.m_temperature = response.getStarTrackerSettings()->getTemperature(); + } + if (featureSettingsKeys.contains("humidity")) { + settings.m_humidity = response.getStarTrackerSettings()->getHumidity(); + } + if (featureSettingsKeys.contains("heightAboveSeaLevel")) { + settings.m_heightAboveSeaLevel = response.getStarTrackerSettings()->getHeightAboveSeaLevel(); + } + if (featureSettingsKeys.contains("temperatureLapseRate")) { + settings.m_temperatureLapseRate = response.getStarTrackerSettings()->getTemperatureLapseRate(); + } + if (featureSettingsKeys.contains("frequency")) { + settings.m_frequency = response.getStarTrackerSettings()->getFrequency() * 100000.0; + } + if (featureSettingsKeys.contains("stellariumServerEnabled")) { + settings.m_enableServer = response.getStarTrackerSettings()->getStellariumServerEnabled() == 1; + } + if (featureSettingsKeys.contains("stellariumPort")) { + settings.m_serverPort = response.getStarTrackerSettings()->getStellariumPort(); + } + if (featureSettingsKeys.contains("updatePeriod")) { + settings.m_updatePeriod = response.getStarTrackerSettings()->getUpdatePeriod(); + } + if (featureSettingsKeys.contains("epoch")) { + settings.m_jnow = *response.getStarTrackerSettings()->getEpoch() == "JNOW"; + } + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getStarTrackerSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getStarTrackerSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getStarTrackerSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getStarTrackerSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getStarTrackerSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getStarTrackerSettings()->getReverseApiDeviceIndex(); + } + if (featureSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIFeatureIndex = response.getStarTrackerSettings()->getReverseApiChannelIndex(); + } +} + +void StarTracker::webapiReverseSendSettings(QList& featureSettingsKeys, const StarTrackerSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("StarTracker")); + swgFeatureSettings->setStarTrackerSettings(new SWGSDRangel::SWGStarTrackerSettings()); + SWGSDRangel::SWGStarTrackerSettings *swgStarTrackerSettings = swgFeatureSettings->getStarTrackerSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("target") || force) { + swgStarTrackerSettings->setTarget(new QString(settings.m_target)); + } + if (featureSettingsKeys.contains("ra") || force) { + swgStarTrackerSettings->setRa(new QString(settings.m_ra)); + } + if (featureSettingsKeys.contains("dec") || force) { + swgStarTrackerSettings->setDec(new QString(settings.m_dec)); + } + if (featureSettingsKeys.contains("latitude") || force) { + swgStarTrackerSettings->setLatitude(settings.m_latitude); + } + if (featureSettingsKeys.contains("longitude") || force) { + swgStarTrackerSettings->setLongitude(settings.m_longitude); + } + if (featureSettingsKeys.contains("dateTime") || force) { + swgStarTrackerSettings->setDateTime(new QString(settings.m_dateTime)); + } + if (featureSettingsKeys.contains("pressure") || force) { + swgStarTrackerSettings->setPressure(settings.m_pressure); + } + if (featureSettingsKeys.contains("temperature") || force) { + swgStarTrackerSettings->setTemperature(settings.m_temperature); + } + if (featureSettingsKeys.contains("humidity") || force) { + swgStarTrackerSettings->setHumidity(settings.m_humidity); + } + if (featureSettingsKeys.contains("heightAboveSeaLevel") || force) { + swgStarTrackerSettings->setHeightAboveSeaLevel(settings.m_heightAboveSeaLevel); + } + if (featureSettingsKeys.contains("temperatureLapseRate") || force) { + swgStarTrackerSettings->setTemperatureLapseRate(settings.m_temperatureLapseRate); + } + if (featureSettingsKeys.contains("frequency") || force) { + swgStarTrackerSettings->setFrequency(settings.m_frequency / 1000000.0); + } + if (featureSettingsKeys.contains("stellariumServerEnabled") || force) { + swgStarTrackerSettings->setStellariumServerEnabled(settings.m_enableServer ? 1 : 0); + } + if (featureSettingsKeys.contains("stellariumPort") || force) { + swgStarTrackerSettings->setStellariumPort(settings.m_serverPort); + } + if (featureSettingsKeys.contains("updatePeriod") || force) { + swgStarTrackerSettings->setUpdatePeriod(settings.m_updatePeriod); + } + if (featureSettingsKeys.contains("epoch") || force) { + swgStarTrackerSettings->setEpoch(settings.m_jnow ? new QString("JNOW") : new QString("J2000")); + } + if (featureSettingsKeys.contains("title") || force) { + swgStarTrackerSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgStarTrackerSettings->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 StarTracker::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "StarTracker::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("StarTracker::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/startracker/startracker.h b/plugins/feature/startracker/startracker.h new file mode 100644 index 000000000..4ad767403 --- /dev/null +++ b/plugins/feature/startracker/startracker.h @@ -0,0 +1,139 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_STARTRACKER_H_ +#define INCLUDE_FEATURE_STARTRACKER_H_ + +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "startrackersettings.h" + +class WebAPIAdapterInterface; +class StarTrackerWorker; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class StarTracker : public Feature +{ + Q_OBJECT +public: + class MsgConfigureStarTracker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const StarTrackerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureStarTracker* create(const StarTrackerSettings& settings, bool force) { + return new MsgConfigureStarTracker(settings, force); + } + + private: + StarTrackerSettings m_settings; + bool m_force; + + MsgConfigureStarTracker(const StarTrackerSettings& 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) + { } + }; + + StarTracker(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~StarTracker(); + 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 StarTrackerSettings& settings); + + static void webapiUpdateFeatureSettings( + StarTrackerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + QThread m_thread; + StarTrackerWorker *m_worker; + StarTrackerSettings m_settings; + bool m_ptt; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const StarTrackerSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const StarTrackerSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_STARTRACKER_H_ diff --git a/plugins/feature/startracker/startracker.qrc b/plugins/feature/startracker/startracker.qrc new file mode 100644 index 000000000..efe342020 --- /dev/null +++ b/plugins/feature/startracker/startracker.qrc @@ -0,0 +1,17 @@ + + + startracker/moon-flat-32.png + startracker/moon-full-32.png + startracker/moon-new-32.png + startracker/moon-old-32.png + startracker/moon-waning-crescent-32.png + startracker/moon-waning-gibbous-32.png + startracker/moon-third-quarter-32.png + startracker/moon-waxing-crescent-32.png + startracker/moon-waxing-gibbous-32.png + startracker/moon-first-quarter-32.png + startracker/moon-young-32.png + startracker/pulsar-32.png + startracker/sun-40.png + + diff --git a/plugins/feature/startracker/startracker/moon-32.png b/plugins/feature/startracker/startracker/moon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..ddd45cb648454d5138629a410c2b0b5925854a6b GIT binary patch literal 1740 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 M07*qoM6N<$g5EaZ#{d8T literal 0 HcmV?d00001 diff --git a/plugins/feature/startracker/startracker/moon-full-32.png b/plugins/feature/startracker/startracker/moon-full-32.png new file mode 100644 index 0000000000000000000000000000000000000000..ddd45cb648454d5138629a410c2b0b5925854a6b GIT binary patch literal 1740 zcmV;-1~d7IP)&YhX}ku=c+DQ;eZg)Rzq;m+!&Vx#^6 z;!<%T2%;b^EQsJjq%G)1L=fE;YZocfiukdR##I|AG$BPORNPceU&(u!&wEFHC+7?= zHMPNk;m*gM^YJ^s^SgIU(=@c7imhNr4ROF_kjUiSYG~wh+d1LsN6F$2z(C|a3)DYd7e{Q6ghO3_k}VS(fo! z1el~L#c_;z&RYBR>eZ_!_l3ZP<>ennQMSEJp=oG)dz+^EeZu@GiWmuo)>^(s5I_wFf-uH>dgREFH~tmC%F4>i&bcpx z_Xfn8nVF&CojX)WJ(1bW>?|8bSw>r1OgP*#Cwe^&00-vdT9Hs62nfNXY5LUR!-p^9 znbSy;B>5tWBIAu=c=1tQ6jbCnzgLLy96_NpKm&pG`+a_g18{>M2m;UGlz9;H174e))@}fIRevXS|(#m4*?kMSDqm7kU+*6fSAGwZ7o*8s@hdsJyaj` z#JkDTi8A7uaflv_=DtqTR_ietCzKAN1QkRZYE35(t*r(I8^hs{15tre=NhPrlT)ab zsc1Fijgmm;aexzEq!{y2DrA9cZ~`L?;~7qMhItrKI(6zLIqfUP`Jj^9g7F%QUTjn$dfnj}*V7AUBADSG%G!8;g5n*lb}d4&)<@9}l*a60a=X!x79?4)NNUP!NSbo#R1raH z<&Y9r6H;xk*8X9gbDyb>Bl}v2gvr?3!APr;Ny3g3StAe#vcwz+8Ahz0+z|*&x+<3h zv~9I5t+gjv12#4`@+`|z77=TA*o%qS1wno#dN|W;VPG^G@i-ojTTU36t9(#Z?e=UIn|5X(X)-S+q5;^KU}9}or~ z*S$U1%1{ZU+*+uc$gdHS>MF^LiwF^m(u6K(TX$sl6h#sCg~!!z8^EDMhb|{caz>R4 zKOlgO#YR}o8CMnE+1%V&7m|d7OUteGU zK7{bBE}3xDc7oJh*K=LKN?||d$09UI9P{;o0|(muWL?+q4+eu>zifN{8Lh3Yoi6hH zZ5PKzcd{xTx~8knD+zmX_^eLb5;Lqvx>(}4P^ZebSC>}v=xz>_7 zT9#W-Hn6Y-d6ss|qbNewXa39nzJJd*Ha4CfkH??abv+=W9vnk@d7aq#MF)Aei0C&W idUG%s{PIAfMD!0@tbN3G4sZ*yJ-@bi|q07g| z`@f`wa{`5uN%O-;a|z^tO8T-w&&?nYFpMMd!J*)zuP-@dUhJ$dq^p5B(j zz}K%|bD3`5xXCq84mfrC6d!|;kr7?v9vw461qKW-4W#8DFzNs%BO{}Mb%4H}-oJse z9Fzl@w6(S04U_}S&CEW69589193U(#JR4jYg#-sv-1Z~4G6LrUH+Q$i^!B}&OAIyP tVPSuzrKMjxp|002ovPDHLkV1oGF7rFod literal 0 HcmV?d00001 diff --git a/plugins/feature/startracker/startracker/moon-old-32.png b/plugins/feature/startracker/startracker/moon-old-32.png new file mode 100644 index 0000000000000000000000000000000000000000..c5898a131b903d9a1029ed2655750094b2f8eb04 GIT binary patch literal 1354 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 U)Ny7M3IG5A07*qoM6N<$f~mc#@&Et; literal 0 HcmV?d00001 diff --git a/plugins/feature/startracker/startracker/moon-waxing-crescent-32.png b/plugins/feature/startracker/startracker/moon-waxing-crescent-32.png new file mode 100644 index 0000000000000000000000000000000000000000..5218137f2fb4a176e11bd5a88d743bb5de04839a GIT binary patch literal 1313 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 O0000X%0UdDF5ryd(JI*{Y?oL#G&6ds_df<1M zujluB-sgFrd0q?vkQB@3^XuOj#>=woWdKmIeHQT?k2D$$y0ox>5C8%Io*;-8j^i(` zE-w0{_?;>sm&@(7noTFeFxXwUd*ZtWg8>Bt0i@IE&?(D;R;&GFGMUU1KY}ymL&#>c zj&LZ{f3UG3D7-M9dA*QGBp{hga+ONOK8|PP%~&-0IS_dJSHyoJ8i_zEm4a5Q^+b|@ zkFi+UX0^_QT*Riw1DQ+)NRm99D4<*}hi6IrE6nB_P%IXKqNu%*0*b}r{WX_sF61H} zZEZoJP=H>qcWp>OI27XNNiIStOp+w~zX>RnN*OuGMUW&3jYi{0;8qD+@m(-@MN&7K zksa*s3xQuBc&k>ckL8SBM6Fg6YGEC3o}4_CGkVdw-7ZqA)lZRqWkr_a8wHA@SnT|P zB1TcvSrIB_7=|e;faiH-1#leqpQm8Z2_1I3V!HstFkWRe8X2Vo;5bgBLZRSSq6rC; z5>30PU`%tgf&x}|| zEltxeC7+B0B;s+)Et`$~SMq2ia@y;~`~sjK8K*L<^G*ny00000NkvXXu0mjf DMiEaF literal 0 HcmV?d00001 diff --git a/plugins/feature/startracker/startracker/pulsar-32.png b/plugins/feature/startracker/startracker/pulsar-32.png new file mode 100644 index 0000000000000000000000000000000000000000..0bae54abcfbb873014bc3c413accf60b1de43a06 GIT binary patch literal 432 zcmV;h0Z;ykP)Px$Vo5|nR9J<*m(fkcFbsx&pTsj6fem1U8=)Jp%8kGXWrMOo*x-1F z9bf_@BU74?^kRr(DY~|?d|&-rc1i#OfFpoA{XH>r#2`E9pWXm?P?&{?!bkvl^I7k2 z0bC2jC;%}@_Za|ZOQeAJf=cjS0FZTb2T+QjnaBYIEVu)#H^8X?5r+XNL`f32dMH0E zOX5-w`rEknufP=AXO2>Qya8=pA zC!)y|(U^gy^5)yMSYna}ZZaZja%N^~jcJvjyIBFAI{B<-1(-Ye{0)Gar3fE)Twe5m zV+v|-9+gJ4=@YTFZFg;}3KuuC!z?@L~G+G^9zCi0000Juu@=8i)ZD1fn}>|L&&$y9*@K{(#ZnyFisdZVH3} z(s(Bs#J-jI_fE>ctKlF9kb4KH9Y}&S{=F9c_hQiB8*zVcCIAKh9W?)U*y`V5>wib> z|DACDcgp?WX^($ry#Af{`*$hi{|?RnJGB1q)cU_m=l^cK_NqHmf!;JL3GxeOVC3TF zyJUzfTW|Nj?v-Nc@pI>+)`V(H>& zPwkg|hSK|%9Auoj;X%REc*n(hJMI;D)JorRp0&}WYTHV2FIMyCnrw3C8G_|@ajSYx zXqlB1$MDleUEh19hZoayt|`J2XT`gJa&#Ik*p(v875}68wxq@V3I>h(7&ilH=H`@o9RUG}3 zyY;xgoOam}vS>?#%_HH-PfS;~FPb;&pU$h@_SzDgo}5^udU{@t%%-nC6VATA@^AlC XvBIqfh3@YKhBbqytDnm{r-UW|VBK!E literal 0 HcmV?d00001 diff --git a/plugins/feature/startracker/startracker/sun-40.png b/plugins/feature/startracker/startracker/sun-40.png new file mode 100644 index 0000000000000000000000000000000000000000..c47094e93c84d275ef5552a7a0f2fc0370556e4e GIT binary patch literal 1170 zcmV;D1a13?P)h*Y zk~t>5S0abr3&-Ko=G1HY3s~+*kRFgg%4ip*EuUgT3ki_5(d&6<*K1}xvpYeek7RAe zV?Tbzuiw|N9cZmbF5S(gMp54~ZG1(d8dg;tF6J@tyVWWqR24|Lq2jRaR&w2qs^nD# z&H|XsnWa^JH(O-@uRs-t3n~s9o+dN=yR71H-s(qxFFS3r_!~6F$N~Yh0Nhq_m}y1_ zwmH3~B`)G5ZXP2Sf;^>wti`j?2CyiDsLAy-v$vh*Kl1M{PW=x~zu!_$8ARSCFy>X4 zfXJ>hA6d++LJ%5|`wvF^g79y_&suU}1+%vFnU*)3)V^ zDJ~tLDSt~Q}l`qq87h@l@2bRZ$clNfqb?J>}>nT zn3#k*c@E-b8~Vh#{zm}c$RO&dIGp8JX_q=XWT$6hj$KU-t}fb8-D${Ljp{}FF2wtt zl9Qi6ADn=mxs*JpZWpq)l@lZBiyJbCXj%20gew}{z{N~Y-0P=9i!=S+@0h;HV1}zu z99%E9M79UIsctgOwBDY_LyPN)q`tH+ZO%U0t~lg*F|WC1&vVrtig>AU}9pzRYF{@K%T(`_G$z1HnaaoaJcAU zA#bm@y*L#`<;o`w|9$}R=H$Dq3V zA`r4>;;24y+>VQxV)rAX{;c@&P3#FXB$Uj5*);6@RVfAz2s{x$*UzSpcX*p!A2!Ua z^PZTH9x6(xhY&c(RnHTbD=;%qKY5r0*{TpS;Zl1k5slF-3^SIvd7i!gUP;@MtO?qt zB&Ah-^5H-M|J#d63YK#AL0jE|yVVU$iIvd|ZsaM^fpN2bMuq8{=7)pgz+*P;)8T-? z66ux+g8Jx`Nu7p;Nw_AhjwLtcms=Z&134+bnx6Be{MTd< z6&{+(o=LKgnc#If{&nV|$s;c1tbXid6Syb3ZD{p-`d~~at#6fMNnNjjY3r_qIp(e= zkarUsYi6AYd8=2KnpW#Krr32bvF{eIt+fK~N&?$_g|V0y^&!MhreqBiL>=l&9F_+c z3iLV&yZb!=H|aXf6{h0Q?tDiEQG@H7=^oDFgwwCi*K>3OcF_&aHj12N*1q^DfbB>t z{9&2k4_#4lxXxqb3M>_7@?hP3-IPHz;OTt5wp`{0%ZIAFe~Y~{?}l}|=>G3>3nZ*$ k=5cJs%IshCzag~7N7g?7vU?Z5uK)l507*qoM6N<$g2kvf%m4rY literal 0 HcmV?d00001 diff --git a/plugins/feature/startracker/startrackergui.cpp b/plugins/feature/startracker/startrackergui.cpp new file mode 100644 index 000000000..460ae2643 --- /dev/null +++ b/plugins/feature/startracker/startrackergui.cpp @@ -0,0 +1,582 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_startrackergui.h" +#include "startracker.h" +#include "startrackergui.h" +#include "startrackerreport.h" +#include "startrackersettingsdialog.h" + +StarTrackerGUI* StarTrackerGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + StarTrackerGUI* gui = new StarTrackerGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void StarTrackerGUI::destroy() +{ + delete this; +} + +void StarTrackerGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray StarTrackerGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool StarTrackerGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +QString StarTrackerGUI::convertDegreesToText(double degrees) +{ + if (m_settings.m_azElUnits == StarTrackerSettings::DMS) + return Units::decimalDegreesToDegreeMinutesAndSeconds(degrees); + else if (m_settings.m_azElUnits == StarTrackerSettings::DM) + return Units::decimalDegreesToDegreesAndMinutes(degrees); + else if (m_settings.m_azElUnits == StarTrackerSettings::D) + return Units::decimalDegreesToDegrees(degrees); + else + return QString("%1").arg(degrees, 0, 'f', 2); +} + +bool StarTrackerGUI::handleMessage(const Message& message) +{ + if (StarTracker::MsgConfigureStarTracker::match(message)) + { + qDebug("StarTrackerGUI::handleMessage: StarTracker::MsgConfigureStarTracker"); + const StarTracker::MsgConfigureStarTracker& cfg = (StarTracker::MsgConfigureStarTracker&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (StarTrackerReport::MsgReportAzAl::match(message)) + { + StarTrackerReport::MsgReportAzAl& azAl = (StarTrackerReport::MsgReportAzAl&) message; + ui->azimuth->setText(convertDegreesToText(azAl.getAzimuth())); + ui->elevation->setText(convertDegreesToText(azAl.getElevation())); + return true; + } + else if (StarTrackerReport::MsgReportRADec::match(message)) + { + StarTrackerReport::MsgReportRADec& raDec = (StarTrackerReport::MsgReportRADec&) message; + m_settings.m_ra = Units::decimalHoursToHoursMinutesAndSeconds(raDec.getRA()); + m_settings.m_dec = Units::decimalDegreesToDegreeMinutesAndSeconds(raDec.getDec()); + ui->rightAscension->setText(m_settings.m_ra); + ui->declination->setText(m_settings.m_dec); + return true; + } + + return false; +} + +void StarTrackerGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void StarTrackerGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +StarTrackerGUI::StarTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::StarTrackerGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + setChannelWidget(false); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + m_starTracker = reinterpret_cast(feature); + m_starTracker->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 chart + m_chart.legend()->hide(); + ui->elevationChart->setChart(&m_chart); + ui->elevationChart->setRenderHint(QPainter::Antialiasing); + m_chart.addAxis(&m_chartXAxis, Qt::AlignBottom); + m_chart.addAxis(&m_chartYAxis, Qt::AlignLeft); + + ui->dateTime->setDateTime(QDateTime::currentDateTime()); + displaySettings(); + applySettings(true); + + // Use My Position from preferences, if none set + if ((m_settings.m_latitude == 0.0) && (m_settings.m_longitude == 0.0)) + on_useMyPosition_clicked(); + +/* + printf("saemundsson=["); + for (int i = 0; i <= 90; i+= 5) + printf("%f ", Astronomy::refractionSaemundsson(i, m_settings.m_pressure, m_settings.m_temperature)); + printf("];\n"); + printf("palRadio=["); + for (int i = 0; i <= 90; i+= 5) + printf("%f ", Astronomy::refractionPAL(i, m_settings.m_pressure, m_settings.m_temperature, m_settings.m_humidity, + 100000000, m_settings.m_latitude, m_settings.m_heightAboveSeaLevel, + m_settings.m_temperatureLapseRate)); + printf("];\n"); + printf("palLight=["); + for (int i = 0; i <= 90; i+= 5) + printf("%f ",Astronomy::refractionPAL(i, m_settings.m_pressure, m_settings.m_temperature, m_settings.m_humidity, + 7.5e14, m_settings.m_latitude, m_settings.m_heightAboveSeaLevel, + m_settings.m_temperatureLapseRate)); + printf("];\n"); +*/ +} + +StarTrackerGUI::~StarTrackerGUI() +{ + delete ui; +} + +void StarTrackerGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void StarTrackerGUI::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_target == "Custom") + { + ui->rightAscension->setText(m_settings.m_ra); + ui->declination->setText(m_settings.m_dec); + } + 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); + } + updateForTarget(); + plotChart(); + blockApplySettings(false); +} + +void StarTrackerGUI::leaveEvent(QEvent*) +{ +} + +void StarTrackerGUI::enterEvent(QEvent*) +{ +} + +void StarTrackerGUI::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 StarTrackerGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + StarTracker::MsgStartStop *message = StarTracker::MsgStartStop::create(checked); + m_starTracker->getInputMessageQueue()->push(message); + } +} + +void StarTrackerGUI::on_latitude_valueChanged(double value) +{ + m_settings.m_latitude = value; + applySettings(); + plotChart(); +} + +void StarTrackerGUI::on_longitude_valueChanged(double value) +{ + m_settings.m_longitude = value; + applySettings(); + plotChart(); +} + +void StarTrackerGUI::on_rightAscension_editingFinished() +{ + m_settings.m_ra = ui->rightAscension->text(); + applySettings(); + plotChart(); +} + +void StarTrackerGUI::on_declination_editingFinished() +{ + m_settings.m_dec = ui->declination->text(); + applySettings(); + plotChart(); +} + +void StarTrackerGUI::updateForTarget() +{ + if (m_settings.m_target == "Sun") + { + ui->rightAscension->setReadOnly(true); + ui->declination->setReadOnly(true); + ui->rightAscension->setText(""); + ui->declination->setText(""); + } + else if (m_settings.m_target == "Moon") + { + ui->rightAscension->setReadOnly(true); + ui->declination->setReadOnly(true); + ui->rightAscension->setText(""); + ui->declination->setText(""); + } + else if (m_settings.m_target == "Custom") + { + ui->rightAscension->setReadOnly(false); + ui->declination->setReadOnly(false); + } + else + { + ui->rightAscension->setReadOnly(true); + ui->declination->setReadOnly(true); + if (m_settings.m_target == "PSR B0329+54") + { + ui->rightAscension->setText("03h32m59.35s"); + ui->declination->setText(QString("54%0134'45.05\"").arg(QChar(0xb0))); + } + else if (m_settings.m_target == "PSR B0833-45") + { + ui->rightAscension->setText("08h35m20.66s"); + ui->declination->setText(QString("-45%0110'35.15\"").arg(QChar(0xb0))); + } + else if (m_settings.m_target == "Sagittarius A") + { + ui->rightAscension->setText("17h45m40.04s"); + ui->declination->setText(QString("-29%0100'28.17\"").arg(QChar(0xb0))); + } + else if (m_settings.m_target == "Cassiopeia A") + { + ui->rightAscension->setText("23h23m24s"); + ui->declination->setText(QString("58%0148'54\"").arg(QChar(0xb0))); + } + else if (m_settings.m_target == "Cygnus A") + { + ui->rightAscension->setText("19h59m28.36s"); + ui->declination->setText(QString("40%0144'02.1\"").arg(QChar(0xb0))); + } + else if (m_settings.m_target == "Taurus A (M1)") + { + ui->rightAscension->setText("05h34m31.94s"); + ui->declination->setText(QString("22%0100'52.2\"").arg(QChar(0xb0))); + } + else if (m_settings.m_target == "Virgo A (M87)") + { + ui->rightAscension->setText("12h30m49.42s"); + ui->declination->setText(QString("12%0123'28.04\"").arg(QChar(0xb0))); + } + on_rightAscension_editingFinished(); + on_declination_editingFinished(); + } + // Clear as no longer valid when target has changed + ui->azimuth->setText(""); + ui->elevation->setText(""); +} + +void StarTrackerGUI::on_target_currentTextChanged(const QString &text) +{ + m_settings.m_target = text; + applySettings(); + updateForTarget(); + plotChart(); +} + +void StarTrackerGUI::updateLST() +{ + QDateTime dt; + + if (m_settings.m_dateTime.isEmpty()) + dt = QDateTime::currentDateTime(); + else + dt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs); + + double lst = Astronomy::localSiderealTime(dt, m_settings.m_longitude); + + ui->lst->setText(Units::decimalHoursToHoursMinutesAndSeconds(lst/15.0, 0)); +} + +void StarTrackerGUI::updateStatus() +{ + int state = m_starTracker->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_starTracker->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } + + updateLST(); +} + +void StarTrackerGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + StarTracker::MsgConfigureStarTracker* message = StarTracker::MsgConfigureStarTracker::create(m_settings, force); + m_starTracker->getInputMessageQueue()->push(message); + } +} + +void StarTrackerGUI::on_useMyPosition_clicked(bool 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 StarTrackerGUI::on_displaySettings_clicked() +{ + StarTrackerSettingsDialog dialog(&m_settings); + if (dialog.exec() == QDialog::Accepted) + { + applySettings(); + } +} + +void StarTrackerGUI::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 StarTrackerGUI::on_dateTime_dateTimeChanged(const QDateTime &datetime) +{ + if (ui->dateTimeSelect->currentIndex() == 1) + { + m_settings.m_dateTime = ui->dateTime->dateTime().toString(Qt::ISODateWithMs); + applySettings(); + plotChart(); + } +} + +// Plot target elevation angle over the day +void StarTrackerGUI::plotChart() +{ + m_chart.removeAllSeries(); + double maxElevation = -90.0; + + QLineSeries *series = new QLineSeries(); + QDateTime dt; + if (m_settings.m_dateTime.isEmpty()) + dt = QDateTime::currentDateTime(); + else + dt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs); + dt.setTime(QTime(0,0)); + QDateTime startTime = dt; + QDateTime endTime = dt; + for (int hour = 0; hour <= 24; hour++) + { + AzAlt aa; + RADec rd; + + // Calculate elevation of desired object + if (m_settings.m_target == "Sun") + Astronomy::sunPosition(aa, rd, m_settings.m_latitude, m_settings.m_longitude, dt); + else if (m_settings.m_target == "Moon") + Astronomy::moonPosition(aa, rd, m_settings.m_latitude, m_settings.m_longitude, dt); + else + { + rd.ra = Astronomy::raToDecimal(m_settings.m_ra); + rd.dec = Astronomy::decToDecimal(m_settings.m_dec); + aa = Astronomy::raDecToAzAlt(rd, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow); + } + + if (aa.alt > maxElevation) + maxElevation = aa.alt; + + // Adjust for refraction + if (m_settings.m_refraction == "Positional Astronomy Library") + { + aa.alt += Astronomy::refractionPAL(aa.alt, m_settings.m_pressure, m_settings.m_temperature, m_settings.m_humidity, + m_settings.m_frequency, m_settings.m_latitude, m_settings.m_heightAboveSeaLevel, + m_settings.m_temperatureLapseRate); + if (aa.alt > 90.0) + aa.alt = 90.0f; + } + else if (m_settings.m_refraction == "Saemundsson") + { + aa.alt += Astronomy::refractionSaemundsson(aa.alt, m_settings.m_pressure, m_settings.m_temperature); + if (aa.alt > 90.0) + aa.alt = 90.0f; + } + + series->append(dt.toMSecsSinceEpoch(), aa.alt); + + endTime = dt; + dt = dt.addSecs(60*60); // addSecs accounts for daylight savings jumps + } + if (maxElevation < 0) + m_chart.setTitle("Not visible from this latitude"); + else + m_chart.setTitle(""); + m_chart.addSeries(series); + series->attachAxis(&m_chartXAxis); + series->attachAxis(&m_chartYAxis); + m_chartXAxis.setTitleText(QString("%1 %2").arg(startTime.date().toString()).arg(startTime.timeZoneAbbreviation())); + m_chartXAxis.setFormat("hh"); + m_chartXAxis.setTickCount(7); + m_chartXAxis.setRange(startTime, endTime); + m_chartYAxis.setRange(0.0, 90.0); + m_chartYAxis.setTitleText(QString("Elevation (%1)").arg(QChar(0xb0))); +} + +// Find target on the Map +void StarTrackerGUI::on_viewOnMap_clicked() +{ + QString target = m_settings.m_target == "Sun" || m_settings.m_target == "Moon" ? m_settings.m_target : "Star"; + FeatureWebAPIUtils::mapFind(target); +} diff --git a/plugins/feature/startracker/startrackergui.h b/plugins/feature/startracker/startrackergui.h new file mode 100644 index 000000000..ced54e3e2 --- /dev/null +++ b/plugins/feature/startracker/startrackergui.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_STARTRACKERGUI_H_ +#define INCLUDE_FEATURE_STARTRACKERGUI_H_ + +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "startrackersettings.h" + +class PluginAPI; +class FeatureUISet; +class StarTracker; + +namespace Ui { + class StarTrackerGUI; +} + +using namespace QtCharts; + +class StarTrackerGUI : public FeatureGUI { + Q_OBJECT +public: + static StarTrackerGUI* 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::StarTrackerGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + StarTrackerSettings m_settings; + bool m_doApplySettings; + + StarTracker* m_starTracker; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + + QChart m_chart; + QDateTimeAxis m_chartXAxis; + QValueAxis m_chartYAxis; + + explicit StarTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~StarTrackerGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void updateForTarget(); + QString convertDegreesToText(double degrees); + bool handleMessage(const Message& message); + void updateLST(); + void plotChart(); + + 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_rightAscension_editingFinished(); + void on_declination_editingFinished(); + void on_target_currentTextChanged(const QString &text); + void on_displaySettings_clicked(); + void on_dateTimeSelect_currentTextChanged(const QString &text); + void on_dateTime_dateTimeChanged(const QDateTime &datetime); + void updateStatus(); + void on_viewOnMap_clicked(); +}; + + +#endif // INCLUDE_FEATURE_STARTRACKERGUI_H_ diff --git a/plugins/feature/startracker/startrackergui.ui b/plugins/feature/startracker/startrackergui.ui new file mode 100644 index 000000000..9e859b468 --- /dev/null +++ b/plugins/feature/startracker/startrackergui.ui @@ -0,0 +1,503 @@ + + + StarTrackerGUI + + + + 0 + 0 + 337 + 568 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 16777215 + 16777215 + + + + + Liberation Sans + 9 + + + + Star Tracker + + + + + 10 + 10 + 301 + 201 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Latitude in decimal degrees (North positive) of observation point / antenna location + + + 6 + + + -90.000000000000000 + + + 90.000000000000000 + + + -90.000000000000000 + + + + + + + Right Ascension of the target object. + +This can be specified as a decimal (E.g. 12.23) or in hours, minutes and seconds (E.g. 12h05m10.2s or 12 05 10.2) + + + 23h59m59.59s + + + + + + + Computed azimuth in degrees to the target from the observation point + + + 360 + + + true + + + + + + + Time + + + + + + + Declination of the target object + +This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and seconds (E.g. 34d12m10.2s, 34d12'10.2" 34 12 10.2) + + + -90d59'59.59" + + + + + + + Target + + + + + + + Date and time to use when calculating target's position + + + dd/MM/yyyy HH:mm:ss + + + true + + + + + + + Latitude + + + + + + + + Now + + + + + Custom + + + + + + + + Elevation + + + + + + + RA + + + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Find target on the map + + + + + + + :/gridpolar.png:/gridpolar.png + + + + + + + Set latitude, longitude and height from My Position in SDRangel preferences + + + + + + + :/import.png:/import.png + + + + + + + Show settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + + + + + + 110 + 0 + + + + 0 + + + + Sun + + + + + Moon + + + + + PSR B0329+54 + + + + + PSR B0833-45 + + + + + Sagittarius A + + + + + Cassiopeia A + + + + + Cygnus A + + + + + Taurus A (M1) + + + + + Virgo A (M87) + + + + + Custom + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Longitude in decimal degress (East positive) of observation point / antenna location + + + 6 + + + -180.000000000000000 + + + 180.000000000000000 + + + -180.000000000000000 + + + + + + + Dec + + + + + + + Longitude + + + + + + + Azimuth + + + + + + + Computed elevation in degrees to the target from the observation point + + + 90 + + + true + + + + + + + LST + + + + + + + Local sidereal time for selected date, time and longitude + + + true + + + + + + + + + + + 10 + 220 + 318 + 268 + + + + + 0 + 0 + + + + + 200 + 200 + + + + Elevation vs Time + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 300 + 250 + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + QChartView + QGraphicsView +
QtCharts
+
+
+ + startStop + useMyPosition + displaySettings + latitude + longitude + target + rightAscension + declination + azimuth + elevation + + + + + +
diff --git a/plugins/feature/startracker/startrackerplugin.cpp b/plugins/feature/startracker/startrackerplugin.cpp new file mode 100644 index 000000000..a96d68484 --- /dev/null +++ b/plugins/feature/startracker/startrackerplugin.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 "startrackergui.h" +#endif +#include "startracker.h" +#include "startrackerplugin.h" +#include "startrackerwebapiadapter.h" + +const PluginDescriptor StarTrackerPlugin::m_pluginDescriptor = { + StarTracker::m_featureId, + QStringLiteral("Star Tracker"), + QStringLiteral("6.4.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +StarTrackerPlugin::StarTrackerPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& StarTrackerPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void StarTrackerPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(StarTracker::m_featureIdURI, StarTracker::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* StarTrackerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* StarTrackerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return StarTrackerGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* StarTrackerPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new StarTracker(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* StarTrackerPlugin::createFeatureWebAPIAdapter() const +{ + return new StarTrackerWebAPIAdapter(); +} diff --git a/plugins/feature/startracker/startrackerplugin.h b/plugins/feature/startracker/startrackerplugin.h new file mode 100644 index 000000000..aff585f7e --- /dev/null +++ b/plugins/feature/startracker/startrackerplugin.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_STARTRACKERPLUGIN_H +#define INCLUDE_FEATURE_STARTRACKERPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class StarTrackerPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.startracker") + +public: + explicit StarTrackerPlugin(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_STARTRACKERPLUGIN_H diff --git a/plugins/feature/startracker/startrackerreport.h b/plugins/feature/startracker/startrackerreport.h new file mode 100644 index 000000000..0cace5aed --- /dev/null +++ b/plugins/feature/startracker/startrackerreport.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// 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_STARTRACKERREPORT_H_ +#define INCLUDE_FEATURE_STARTRACKERREPORT_H_ + +#include + +#include "util/message.h" + +class StarTrackerReport : public QObject +{ + Q_OBJECT +public: + class MsgReportAzAl : public Message { + MESSAGE_CLASS_DECLARATION + + public: + double getAzimuth() const { return m_azimuth; } + double getElevation() const { return m_elevation; } + + static MsgReportAzAl* create(double azimuth, double elevation) + { + return new MsgReportAzAl(azimuth, elevation); + } + + private: + double m_azimuth; + double m_elevation; + + MsgReportAzAl(double azimuth, double elevation) : + Message(), + m_azimuth(azimuth), + m_elevation(elevation) + { + } + }; + + class MsgReportRADec : public Message { + MESSAGE_CLASS_DECLARATION + + public: + double getRA() const { return m_ra; } + double getDec() const { return m_dec; } + + static MsgReportRADec* create(double ra, double dec) + { + return new MsgReportRADec(ra, dec); + } + + private: + double m_ra; + double m_dec; + + MsgReportRADec(double ra, double dec) : + Message(), + m_ra(ra), + m_dec(dec) + { + } + }; + +public: + StarTrackerReport() {} + ~StarTrackerReport() {} +}; + +#endif // INCLUDE_FEATURE_STARTRACKERREPORT_H_ diff --git a/plugins/feature/startracker/startrackersettings.cpp b/plugins/feature/startracker/startrackersettings.cpp new file mode 100644 index 000000000..2585a4759 --- /dev/null +++ b/plugins/feature/startracker/startrackersettings.cpp @@ -0,0 +1,166 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "startrackersettings.h" + +StarTrackerSettings::StarTrackerSettings() +{ + resetToDefaults(); +} + +void StarTrackerSettings::resetToDefaults() +{ + m_ra = ""; + m_dec = ""; + m_latitude = 0.0; + m_longitude = 0.0; + m_target = "Sun"; + m_dateTime = ""; + m_refraction = "Positional Astronomy Library"; + m_pressure = 1010; + m_temperature = 10; + m_humidity = 80.0; + m_heightAboveSeaLevel = 0.0; + m_temperatureLapseRate = 6.49; + m_frequency = 435000000; + m_enableServer = true; + m_serverPort = 10001; + m_azElUnits = DM; + m_updatePeriod = 1.0; + m_jnow = false; + m_drawSunOnMap = true; + m_drawMoonOnMap = true; + m_drawStarOnMap = true; + m_title = "Star 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; +} + +QByteArray StarTrackerSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_ra); + s.writeString(2, m_dec); + s.writeDouble(3, m_latitude); + s.writeDouble(4, m_longitude); + s.writeString(5, m_target); + s.writeString(6, m_dateTime); + s.writeU32(7, m_enableServer); + s.writeU32(8, m_serverPort); + s.writeS32(9, m_azElUnits); + s.writeFloat(10, m_updatePeriod); + s.writeBool(11, m_jnow); + s.writeString(12, m_refraction); + s.writeDouble(13, m_pressure); + s.writeDouble(14, m_temperature); + s.writeDouble(15, m_humidity); + s.writeDouble(16, m_heightAboveSeaLevel); + s.writeDouble(17, m_temperatureLapseRate); + s.writeDouble(18, m_frequency); + s.writeBool(19, m_drawSunOnMap); + s.writeBool(20, m_drawMoonOnMap); + s.writeBool(21, m_drawStarOnMap); + s.writeString(22, m_title); + s.writeU32(23, m_rgbColor); + s.writeBool(24, m_useReverseAPI); + s.writeString(25, m_reverseAPIAddress); + s.writeU32(26, m_reverseAPIPort); + s.writeU32(27, m_reverseAPIFeatureSetIndex); + s.writeU32(28, m_reverseAPIFeatureIndex); + + return s.final(); +} + +bool StarTrackerSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readString(1, &m_ra, ""); + d.readString(2, &m_dec, ""); + d.readDouble(3, &m_latitude, 0.0); + d.readDouble(4, &m_longitude, 0.0); + d.readString(5, &m_target, "Sun"); + d.readString(6, &m_dateTime, ""); + d.readBool(7, &m_enableServer, true); + d.readU32(8, &utmp, 0); + if ((utmp > 1023) && (utmp < 65535)) { + m_serverPort = utmp; + } else { + m_serverPort = 10001; + } + d.readS32(9, (qint32 *)&m_azElUnits, DM); + d.readFloat(10, &m_updatePeriod, 1.0f); + d.readBool(11, &m_jnow, false); + d.readString(12, &m_refraction, "Positional Astronomy Library"); + d.readDouble(13, &m_pressure, 1010); + d.readDouble(14, &m_temperature, 10); + d.readDouble(15, &m_humidity, 10); + d.readDouble(16, &m_heightAboveSeaLevel, 80); + d.readDouble(17, &m_temperatureLapseRate, 6.49); + d.readDouble(18, &m_frequency, 435000000.0); + d.readBool(19, &m_drawSunOnMap, true); + d.readBool(20, &m_drawMoonOnMap, true); + d.readBool(21, &m_drawStarOnMap, true); + + d.readString(22, &m_title, "Star Tracker"); + d.readU32(23, &m_rgbColor, QColor(225, 25, 99).rgb()); + d.readBool(24, &m_useReverseAPI, false); + d.readString(25, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(26, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(27, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(28, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/feature/startracker/startrackersettings.h b/plugins/feature/startracker/startrackersettings.h new file mode 100644 index 000000000..17290f603 --- /dev/null +++ b/plugins/feature/startracker/startrackersettings.h @@ -0,0 +1,67 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_STARTRACKERSETTINGS_H_ +#define INCLUDE_FEATURE_STARTRACKERSETTINGS_H_ + +#include +#include + +#include "util/message.h" + +class Serializable; + +struct StarTrackerSettings +{ + QString m_ra; + QString m_dec; + double m_latitude; + double m_longitude; + QString m_target; // Sun, Moon, Custom + QString m_dateTime; // Date/time for observation, or "" for now + QString m_refraction; // Refraction correction. "None", "Saemundsson" or "Positional Astronomy Library" + double m_pressure; // Air pressure in millibars + double m_temperature; // Air temperature in C + double m_humidity; // Humidity in % + double m_heightAboveSeaLevel; // In metres + double m_temperatureLapseRate; // In K/km + double m_frequency; // Observation frequency in Hz + uint16_t m_serverPort; + bool m_enableServer; // Enable Stellarium server + enum AzElUnits {DMS, DM, D, Decimal} m_azElUnits; + float m_updatePeriod; + bool m_jnow; // Use JNOW epoch rather than J2000 + bool m_drawSunOnMap; + bool m_drawMoonOnMap; + bool m_drawStarOnMap; + + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + + StarTrackerSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif // INCLUDE_FEATURE_STARTRACKERSETTINGS_H_ diff --git a/plugins/feature/startracker/startrackersettingsdialog.cpp b/plugins/feature/startracker/startrackersettingsdialog.cpp new file mode 100644 index 000000000..5cd7a127c --- /dev/null +++ b/plugins/feature/startracker/startrackersettingsdialog.cpp @@ -0,0 +1,68 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "startrackersettingsdialog.h" +#include + +StarTrackerSettingsDialog::StarTrackerSettingsDialog(StarTrackerSettings *settings, + QWidget* parent) : + QDialog(parent), + ui(new Ui::StarTrackerSettingsDialog), + m_settings(settings) +{ + ui->setupUi(this); + ui->epoch->setCurrentIndex(settings->m_jnow ? 1 : 0); + ui->azElUnits->setCurrentIndex((int)settings->m_azElUnits); + ui->updatePeriod->setValue(settings->m_updatePeriod); + ui->serverPort->setValue(settings->m_serverPort); + ui->enableServer->setChecked(settings->m_enableServer); + ui->refraction->setCurrentIndex(ui->refraction->findText(settings->m_refraction)); + ui->pressure->setValue(settings->m_pressure); + ui->temperature->setValue(settings->m_temperature); + ui->humidity->setValue(settings->m_humidity); + ui->height->setValue(settings->m_heightAboveSeaLevel); + ui->temperatureLapseRate->setValue(settings->m_temperatureLapseRate); + ui->frequency->setValue(settings->m_frequency/1000000.0); + ui->drawSunOnMap->setChecked(settings->m_drawSunOnMap); + ui->drawMoonOnMap->setChecked(settings->m_drawMoonOnMap); + ui->drawStarOnMap->setChecked(settings->m_drawStarOnMap); +} + +StarTrackerSettingsDialog::~StarTrackerSettingsDialog() +{ + delete ui; +} + +void StarTrackerSettingsDialog::accept() +{ + m_settings->m_jnow = ui->epoch->currentIndex() == 1; + m_settings->m_azElUnits = (StarTrackerSettings::AzElUnits)ui->azElUnits->currentIndex(); + m_settings->m_updatePeriod = ui->updatePeriod->value(); + m_settings->m_serverPort = (uint16_t)ui->serverPort->value(); + m_settings->m_enableServer = ui->enableServer->isChecked(); + m_settings->m_refraction = ui->refraction->currentText(); + m_settings->m_pressure = ui->pressure->value(); + m_settings->m_temperature = ui->temperature->value(); + m_settings->m_humidity = ui->humidity->value(); + m_settings->m_heightAboveSeaLevel = ui->height->value(); + m_settings->m_temperatureLapseRate = ui->temperatureLapseRate->value(); + m_settings->m_frequency = ui->frequency->value() * 1000000.0; + m_settings->m_drawSunOnMap = ui->drawSunOnMap->isChecked(); + m_settings->m_drawMoonOnMap = ui->drawMoonOnMap->isChecked(); + m_settings->m_drawStarOnMap = ui->drawStarOnMap->isChecked(); + QDialog::accept(); +} diff --git a/plugins/feature/startracker/startrackersettingsdialog.h b/plugins/feature/startracker/startrackersettingsdialog.h new file mode 100644 index 000000000..25fe401c7 --- /dev/null +++ b/plugins/feature/startracker/startrackersettingsdialog.h @@ -0,0 +1,40 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_STARTRACKERSETTINGSDIALOG_H +#define INCLUDE_STARTRACKERSETTINGSDIALOG_H + +#include "ui_startrackersettingsdialog.h" +#include "startrackersettings.h" + +class StarTrackerSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit StarTrackerSettingsDialog(StarTrackerSettings* settings, QWidget* parent = 0); + ~StarTrackerSettingsDialog(); + + StarTrackerSettings *m_settings; + +private slots: + void accept(); + +private: + Ui::StarTrackerSettingsDialog* ui; +}; + +#endif // INCLUDE_STARTRACKERSETTINGSDIALOG_H diff --git a/plugins/feature/startracker/startrackersettingsdialog.ui b/plugins/feature/startracker/startrackersettingsdialog.ui new file mode 100644 index 000000000..8c1c15a72 --- /dev/null +++ b/plugins/feature/startracker/startrackersettingsdialog.ui @@ -0,0 +1,382 @@ + + + StarTrackerSettingsDialog + + + + 0 + 0 + 351 + 468 + + + + + Liberation Sans + 9 + + + + Star Tracker Settings + + + + + + + + + Height above sea level (m) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Draw target star on map + + + + + + + Air temperature (C) + + + + + + + Refraction correction + + + + + + + Stellarium telescope server IP port number + + + 1024 + + + 65535 + + + 10001 + + + + + + + Radio frequency being observed + + + 1000000000 + + + 435 + + + + + + + Air pressure (mb) + + + + + + + Air pressure in millibars, for use in atmospheric refraction correction + + + 2000.000000000000000 + + + 1.000000000000000 + + + 1010.000000000000000 + + + + + + + Epoch for RA & Dec + + + + + + + Air temperature in degrees Celsius, for use in atmospheric refraction correction + + + -100 + + + 100 + + + 10 + + + + + + + Draw Sun on map + + + + + + + Draw Moon on map + + + + + + + Relative humidity in % + + + 100 + + + 80 + + + + + + + Stellarium server port + + + + + + + Update period (s) + + + + + + + Humidity (%) + + + + + + + Enter the time in seconds between each calculation of the target's position + + + 1.000000000000000 + + + + + + + Height of observation/antenna location above sea level in metres + + + -1000 + + + 20000 + + + + + + + Frequency (MHz) + + + + + + + Epoch for custom right ascension and declination + + + + J2000 + + + + + JNOW + + + + + + + + Azimuth and elevation units + + + + + + + Atmospheric refraction correction + + + 0 + + + + None + + + + + Saemundsson + + + + + Positional Astronomy Library + + + + + + + + Enable Stellarium server which allows RA and Dec to be sent to and from Stellarium + + + Stellarium server + + + + + + + Units used for displaying azimuth and elevation. Either degrees, minutes and seconds or decimal degrees. + + + false + + + + ° ' " + + + + + ° ' + + + + + ° + + + + + Decimal + + + + + + + + Temperature lapse rate (K/m) + + + Temperature lapse rate (K/km) + + + + + + + 3 + + + 100.000000000000000 + + + 6.490000000000000 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + StarTrackerSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + StarTrackerSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/startracker/startrackerwebapiadapter.cpp b/plugins/feature/startracker/startrackerwebapiadapter.cpp new file mode 100644 index 000000000..002c8c775 --- /dev/null +++ b/plugins/feature/startracker/startrackerwebapiadapter.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 "startracker.h" +#include "startrackerwebapiadapter.h" + +StarTrackerWebAPIAdapter::StarTrackerWebAPIAdapter() +{} + +StarTrackerWebAPIAdapter::~StarTrackerWebAPIAdapter() +{} + +int StarTrackerWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings()); + response.getSimplePttSettings()->init(); + StarTracker::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int StarTrackerWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + StarTracker::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/startracker/startrackerwebapiadapter.h b/plugins/feature/startracker/startrackerwebapiadapter.h new file mode 100644 index 000000000..6e30ee297 --- /dev/null +++ b/plugins/feature/startracker/startrackerwebapiadapter.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_STARTRACKER_WEBAPIADAPTER_H +#define INCLUDE_STARTRACKER_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "startrackersettings.h" + +/** + * Standalone API adapter only for the settings + */ +class StarTrackerWebAPIAdapter : public FeatureWebAPIAdapter { +public: + StarTrackerWebAPIAdapter(); + virtual ~StarTrackerWebAPIAdapter(); + + 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: + StarTrackerSettings m_settings; +}; + +#endif // INCLUDE_STARTRACKER_WEBAPIADAPTER_H diff --git a/plugins/feature/startracker/startrackerworker.cpp b/plugins/feature/startracker/startrackerworker.cpp new file mode 100644 index 000000000..268a2a3fb --- /dev/null +++ b/plugins/feature/startracker/startrackerworker.cpp @@ -0,0 +1,551 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "maincore.h" + +#include "startracker.h" +#include "startrackerworker.h" +#include "startrackerreport.h" + +MESSAGE_CLASS_DEFINITION(StarTrackerWorker::MsgConfigureStarTrackerWorker, Message) +MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportAzAl, Message) +MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportRADec, Message) + +StarTrackerWorker::StarTrackerWorker(StarTracker* starTracker, WebAPIAdapterInterface *webAPIAdapterInterface) : + m_starTracker(starTracker), + m_webAPIAdapterInterface(webAPIAdapterInterface), + m_msgQueueToFeature(nullptr), + m_msgQueueToGUI(nullptr), + m_running(false), + m_mutex(QMutex::Recursive), + m_tcpServer(nullptr), + m_clientConnection(nullptr) +{ + connect(&m_pollTimer, SIGNAL(timeout()), this, SLOT(update())); +} + +StarTrackerWorker::~StarTrackerWorker() +{ + m_inputMessageQueue.clear(); +} + +void StarTrackerWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +bool StarTrackerWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_pollTimer.start((int)round(m_settings.m_updatePeriod*1000.0)); + m_running = true; + return m_running; +} + +void StarTrackerWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + restartServer(false, 0); + m_pollTimer.stop(); + m_running = false; +} + +void StarTrackerWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool StarTrackerWorker::handleMessage(const Message& cmd) +{ + if (MsgConfigureStarTrackerWorker::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureStarTrackerWorker& cfg = (MsgConfigureStarTrackerWorker&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else + { + return false; + } +} + +void StarTrackerWorker::applySettings(const StarTrackerSettings& settings, bool force) +{ + qDebug() << "StarTrackerWorker::applySettings:" + << " m_target: " << settings.m_target + << " m_ra: " << settings.m_ra + << " m_dec: " << settings.m_dec + << " m_time: " << settings.m_dateTime + << " m_enableServer: " << settings.m_enableServer + << " m_serverPort: " << settings.m_serverPort + << " 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_dateTime != settings.m_dateTime) + || (m_settings.m_refraction != settings.m_refraction) + || (m_settings.m_pressure != settings.m_pressure) + || (m_settings.m_temperature != settings.m_temperature) + || (m_settings.m_ra != settings.m_ra) + || (m_settings.m_dec != settings.m_dec) || force) + { + // Recalculate immediately + QTimer::singleShot(1, this, &StarTrackerWorker::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_drawSunOnMap && m_settings.m_drawSunOnMap) + removeFromMap("Sun"); + if (!settings.m_drawMoonOnMap && m_settings.m_drawMoonOnMap) + removeFromMap("Moon"); + if ((!settings.m_drawStarOnMap && m_settings.m_drawStarOnMap) + || (((settings.m_target == "Sun") || (settings.m_target == "Moon")) + && ((m_settings.m_target != "Sun") && (m_settings.m_target != "Moon")))) + removeFromMap("Star"); + + if ((settings.m_serverPort != m_settings.m_serverPort) || + (settings.m_enableServer != m_settings.m_enableServer) || force) + { + restartServer(settings.m_enableServer, settings.m_serverPort); + } + + m_settings = settings; +} + +void StarTrackerWorker::restartServer(bool enabled, uint32_t port) +{ + if (m_tcpServer) + { + if (m_clientConnection) + { + m_clientConnection->close(); + delete m_clientConnection; + m_clientConnection = nullptr; + } + + disconnect(m_tcpServer, &QTcpServer::newConnection, this, &StarTrackerWorker::acceptConnection); + m_tcpServer->close(); + delete m_tcpServer; + m_tcpServer = nullptr; + } + + if (enabled) + { + qDebug() << "StarTrackerWorker::restartServer: server enabled on port " << port; + m_tcpServer = new QTcpServer(this); + + if (!m_tcpServer->listen(QHostAddress::Any, port)) { + qWarning("Star Tracker failed to listen on port %u. Check it is not already in use.", port); + } else { + connect(m_tcpServer, &QTcpServer::newConnection, this, &StarTrackerWorker::acceptConnection); + } + } +} + +void StarTrackerWorker::acceptConnection() +{ + QMutexLocker mutexLocker(&m_mutex); + m_clientConnection = m_tcpServer->nextPendingConnection(); + + if (!m_clientConnection) { + return; + } + + connect(m_clientConnection, &QIODevice::readyRead, this, &StarTrackerWorker::readStellariumCommand); + connect(m_clientConnection, SIGNAL(disconnected()), this, SLOT(disconnected())); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + connect(m_clientConnection, QOverload::of(&QAbstractSocket::error), this, &StarTrackerWorker::errorOccurred); +#else + connect(m_clientConnection, &QAbstractSocket::errorOccurred, this, &StarTrackerWorker::errorOccurred); +#endif + qDebug() << "StarTrackerWorker::acceptConnection: client connected"; +} + +void StarTrackerWorker::disconnected() +{ + QMutexLocker mutexLocker(&m_mutex); + qDebug() << "StarTrackerWorker::disconnected"; + m_clientConnection->deleteLater(); + m_clientConnection = nullptr; + /*if (m_msgQueueToFeature) + { + StarTrackerWorker::MsgReportWorker *msg = StarTrackerWorker::MsgReportWorker::create("Disconnected"); + m_msgQueueToFeature->push(msg); + }*/ +} + +void StarTrackerWorker::errorOccurred(QAbstractSocket::SocketError socketError) +{ + qDebug() << "StarTrackerWorker::errorOccurred: " << socketError; + /*if (m_msgQueueToFeature) + { + StarTrackerWorker::MsgReportWorker *msg = StarTrackerWorker::MsgReportWorker::create(m_socket.errorString() + " " + socketError); + m_msgQueueToFeature->push(msg); + }*/ +} + +// Get RA & Dec from Stellarium +// Protocol described here: +// http://svn.code.sf.net/p/stellarium/code/trunk/telescope_server/stellarium_telescope_protocol.txt +void StarTrackerWorker::readStellariumCommand() +{ + QMutexLocker mutexLocker(&m_mutex); + + unsigned char buf[64]; + qint64 len; + + len = m_clientConnection->read((char *)buf, sizeof(buf)); + if (len != -1) + { + int msg_len; + int msg_type; + unsigned char *msg; + + // Extract length and message type + msg_len = buf[0] | (buf[1] << 8); + msg_type = buf[2] | (buf[3] << 8); + msg = &buf[4]; + + if (msg_type == 0) // MessageGoto + { + unsigned ra; + int dec; + + if (msg_len == 20) + { + // Skip time + msg += 8; + // Extract RA LSB first + ra = msg[0] | (msg[1] << 8) | (msg[2] << 16) | (msg[3] << 24); + msg += 4; + // Extract DEC LSB first + dec = msg[0] | (msg[1] << 8) | (msg[2] << 16) | (msg[3] << 24); + msg += 4; + + // Convert from integer to floating point + double raDeg = ra*(24.0/4294967296.0); // Convert to decimal hours + double decDeg = dec*(360.0/4294967296.0); // Convert to decimal degrees + + // Set as current target + m_settings.m_ra = Units::decimalHoursToHoursMinutesAndSeconds(raDeg); + m_settings.m_dec = Units::decimalDegreesToDegreeMinutesAndSeconds(decDeg); + + qDebug() << "StarTrackerWorker: New target from Stellarum: " << m_settings.m_ra << " " << m_settings.m_dec; + + // Forward to GUI for display + if (getMessageQueueToGUI()) + getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(raDeg, decDeg)); + } + else + { + qDebug() << "StarTrackerWorker: Unexpected number of bytes received (" << len << ") for message type: " << msg_type; + } + } + else + { + qDebug() << "StarTrackerWorker: Unsupported Stellarium message type: " << msg_type; + } + } +} + +// Send our target to Stellarium (J2000 epoch) +void StarTrackerWorker::writeStellariumTarget(double ra, double dec) +{ + QMutexLocker mutexLocker(&m_mutex); + + if (m_clientConnection != nullptr) + { + unsigned char buf[24]; + + // Length + buf[0] = sizeof(buf); + buf[1] = 0; + // Type (MessageCurrentPosition) + buf[2] = 0; + buf[3] = 0; + // Time (unused) + buf[4] = 0; + buf[5] = 0; + buf[6] = 0; + buf[7] = 0; + buf[8] = 0; + buf[9] = 0; + buf[10] = 0; + buf[11] = 0; + // RA + unsigned raInt = ra * (4294967296.0/24.0); + buf[12] = raInt & 0xff; + buf[13] = (raInt >> 8) & 0xff; + buf[14] = (raInt >> 16) & 0xff; + buf[15] = (raInt >> 24) & 0xff; + // Dec + int decInt = dec * (4294967296.0/360.0); + buf[16] = decInt & 0xff; + buf[17] = (decInt >> 8) & 0xff; + buf[18] = (decInt >> 16) & 0xff; + buf[19] = (decInt >> 24) & 0xff; + // Status (OK) + buf[20] = 0; + buf[21] = 0; + buf[22] = 0; + buf[23] = 0; + + m_clientConnection->write((char *)buf, sizeof(buf)); + } +} + + +void StarTrackerWorker::updateRaDec(RADec rd, QDateTime dt) +{ + RADec rdJ2000; + double jd; + + jd = Astronomy::julianDate(dt); + // Precess to J2000 + rdJ2000 = Astronomy::precess(rd, jd, Astronomy::jd_j2000()); + // Send to Stellarium + writeStellariumTarget(rdJ2000.ra, rdJ2000.dec); + // Send to GUI + if (m_settings.m_target == "Sun" || m_settings.m_target == "Moon") + { + if (getMessageQueueToGUI()) + { + if (m_settings.m_jnow) + getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(rd.ra, rd.dec)); + else + getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(rdJ2000.ra, rdJ2000.dec)); + } + } +} + +void StarTrackerWorker::removeFromMap(QString id) +{ + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_starTracker, "mapitems"); + if (mapMessageQueues) + { + sendToMap(mapMessageQueues, id, "", "", 0.0, 0.0); + } +} + +void StarTrackerWorker::sendToMap(QList *mapMessageQueues, QString name, QString image, QString text, double lat, double lon, double rotation) +{ + 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->setImage(new QString(image)); + swgMapItem->setImageRotation(rotation); + swgMapItem->setText(new QString(text)); + swgMapItem->setImageFixedSize(1); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_starTracker, swgMapItem); + (*it)->push(msg); + } +} + +QString moonPhase(double sunLongitude, double moonLongitude, double observationLatitude, double &rotation) +{ + double difference = sunLongitude - moonLongitude; + if (difference < -180.0) + difference += 360.0; + else if (difference > 180.0) + difference -= 360.0; + + if (difference >= 0.0) + rotation = observationLatitude - 90.0; + else + rotation = 90.0 - observationLatitude; + + // These probably shouldn't be divided equally + if (difference < -157.5) + return "full"; + else if (difference < -112.5) + return "waxing-gibbous"; + else if (difference < -67.5) + return "first-quarter"; + else if (difference < -22.5) + return "waxing-crescent"; + else if (difference < 22.5) + return "new"; + else if (difference < 67.5) + return "waning-crescent"; + else if (difference < 112.5) + return "third-quarter"; + else if (difference < 157.5) + return "waning-gibbous"; + else if (difference < 202.5) + return "full"; + else + return "full"; +} + +void StarTrackerWorker::update() +{ + AzAlt aa, sunAA, moonAA; + RADec rd, sunRD, moonRD; + + QDateTime dt; + + // Get date and time to calculate position at + if (m_settings.m_dateTime == "") + dt = QDateTime::currentDateTime(); + else + dt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs); + + // Calculate position + if ((m_settings.m_target == "Sun") || (m_settings.m_drawSunOnMap)) + Astronomy::sunPosition(sunAA, sunRD, m_settings.m_latitude, m_settings.m_longitude, dt); + if ((m_settings.m_target == "Moon") || (m_settings.m_drawMoonOnMap)) + Astronomy::moonPosition(moonAA, moonRD, m_settings.m_latitude, m_settings.m_longitude, dt); + + if (m_settings.m_target == "Sun") + { + rd = sunRD; + aa = sunAA; + } + else if (m_settings.m_target == "Moon") + { + rd = moonRD; + aa = moonAA; + } + else + { + // Convert RA/Dec to Alt/Az + rd.ra = Astronomy::raToDecimal(m_settings.m_ra); + rd.dec = Astronomy::decToDecimal(m_settings.m_dec); + aa = Astronomy::raDecToAzAlt(rd, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow); + } + updateRaDec(rd, dt); + + // Adjust for refraction + if (m_settings.m_refraction == "Positional Astronomy Library") + { + aa.alt += Astronomy::refractionPAL(aa.alt, m_settings.m_pressure, m_settings.m_temperature, m_settings.m_humidity, + m_settings.m_frequency, m_settings.m_latitude, m_settings.m_heightAboveSeaLevel, + m_settings.m_temperatureLapseRate); + if (aa.alt > 90.0) + aa.alt = 90.0f; + } + else if (m_settings.m_refraction == "Saemundsson") + { + aa.alt += Astronomy::refractionSaemundsson(aa.alt, m_settings.m_pressure, m_settings.m_temperature); + if (aa.alt > 90.0) + aa.alt = 90.0f; + } + + // Send to GUI + if (getMessageQueueToGUI()) + { + StarTrackerReport::MsgReportAzAl *msg = StarTrackerReport::MsgReportAzAl::create(aa.az, aa.alt); + getMessageQueueToGUI()->push(msg); + } + + // Send Az/El to Rotator Controllers + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_starTracker, "target"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation(); + swgTarget->setName(new QString(m_settings.m_target)); + swgTarget->setAzimuth(aa.az); + swgTarget->setElevation(aa.alt); + (*it)->push(MainCore::MsgTargetAzimuthElevation::create(m_starTracker, swgTarget)); + } + } + + // Send to Map + if (m_settings.m_drawSunOnMap || m_settings.m_drawMoonOnMap || m_settings.m_drawStarOnMap) + { + mapMessageQueues = messagePipes.getMessageQueues(m_starTracker, "mapitems"); + if (mapMessageQueues) + { + // Different between GMST(Lst at Greenwich) and RA + double lst = Astronomy::localSiderealTime(dt, 0.0); + double sunLongitude; + double sunLatitude; + + if (m_settings.m_drawSunOnMap || m_settings.m_drawMoonOnMap) + { + sunLongitude = Astronomy::lstAndRAToLongitude(lst, sunRD.ra); + sunLatitude = sunRD.dec; + sendToMap(mapMessageQueues, "Sun", "qrc:///startracker/startracker/sun-40.png", "Sun", sunLatitude, sunLongitude); + } + if (m_settings.m_drawMoonOnMap) + { + double moonLongitude = Astronomy::lstAndRAToLongitude(lst, moonRD.ra); + double moonLatitude = moonRD.dec; + double moonRotation; + QString phase = moonPhase(sunLongitude, moonLongitude, m_settings.m_latitude, moonRotation); + sendToMap(mapMessageQueues, "Moon", QString("qrc:///startracker/startracker/moon-%1-32").arg(phase), "Moon", + moonLatitude, moonLongitude, moonRotation); + } + if ((m_settings.m_drawStarOnMap) && (m_settings.m_target != "Sun") && (m_settings.m_target != "Moon")) + { + double starLongitude = Astronomy::lstAndRAToLongitude(lst, rd.ra); + double starLatitude = rd.dec; + QString text = m_settings.m_target == "Custom" ? "Star" : m_settings.m_target; + sendToMap(mapMessageQueues, "Star", "qrc:///startracker/startracker/pulsar-32.png", text, starLatitude, starLongitude); + } + } + } + +} diff --git a/plugins/feature/startracker/startrackerworker.h b/plugins/feature/startracker/startrackerworker.h new file mode 100644 index 000000000..591b0cbd9 --- /dev/null +++ b/plugins/feature/startracker/startrackerworker.h @@ -0,0 +1,109 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_STARTRACKERWORKER_H_ +#define INCLUDE_FEATURE_STARTRACKERWORKER_H_ + +#include +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" +#include "util/astronomy.h" + +#include "startrackersettings.h" + +class WebAPIAdapterInterface; +class QTcpServer; +class QTcpSocket; +class StarTracker; +class QDateTime; + +class StarTrackerWorker : public QObject +{ + Q_OBJECT +public: + class MsgConfigureStarTrackerWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const StarTrackerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureStarTrackerWorker* create(const StarTrackerSettings& settings, bool force) + { + return new MsgConfigureStarTrackerWorker(settings, force); + } + + private: + StarTrackerSettings m_settings; + bool m_force; + + MsgConfigureStarTrackerWorker(const StarTrackerSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + StarTrackerWorker(StarTracker* starTracker, WebAPIAdapterInterface *webAPIAdapterInterface); + ~StarTrackerWorker(); + 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: + + StarTracker* m_starTracker; + 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; + StarTrackerSettings m_settings; + bool m_running; + QMutex m_mutex; + QTimer m_pollTimer; + QTcpServer *m_tcpServer; + QTcpSocket *m_clientConnection; + + bool handleMessage(const Message& cmd); + void applySettings(const StarTrackerSettings& settings, bool force = false); + void restartServer(bool enabled, uint32_t port); + MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; } + void setAzimuth(int azimuth); + void setAzimuthElevation(int azimuth, int elevation); + void updateRaDec(RADec rd, QDateTime dt); + void writeStellariumTarget(double ra, double dec); + void removeFromMap(QString id); + void sendToMap(QList *mapMessageQueues, QString id, QString image, QString text, double lat, double lon, double rotation=0.0); + +private slots: + void handleInputMessages(); + void update(); + void acceptConnection(); + void disconnected(); + void errorOccurred(QAbstractSocket::SocketError socketError); + void readStellariumCommand(); +}; + +#endif // INCLUDE_FEATURE_STARTRACKERWORKER_H_ diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 2735611a2..35b21ce28 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -176,6 +176,8 @@ set(sdrbase_SOURCES settings/mainsettings.cpp util/ax25.cpp + util/aprs.cpp + util/astronomy.cpp util/azel.cpp util/crc.cpp util/CRC64.cpp @@ -366,6 +368,8 @@ set(sdrbase_HEADERS settings/mainsettings.h util/ax25.h + util/aprs.h + util/astronomy.h util/azel.h util/CRC64.h util/csv.h diff --git a/sdrbase/util/astronomy.cpp b/sdrbase/util/astronomy.cpp new file mode 100644 index 000000000..76544449b --- /dev/null +++ b/sdrbase/util/astronomy.cpp @@ -0,0 +1,896 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2004 Rutherford Appleton Laboratory // +// Copyright (C) 2012 Science and Technology Facilities Council. // +// // +// 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 "util/units.h" +#include "astronomy.h" + +// Function prototypes +static void palRefz(double zu, double refa, double refb, double *zr); +static void palRefco (double hm, double tdk, double pmb, double rh, + double wl, double phi, double tlr, double eps, + double *refa, double *refb); +static void palRefro( double zobs, double hm, double tdk, double pmb, + double rh, double wl, double phi, double tlr, + double eps, double * ref); + + +// Calculate Julian date (days since January 1, 4713 BC) from Gregorian calendar date +double Astronomy::julianDate(int year, int month, int day, int hours, int minutes, int seconds) +{ + int julian_day; + double julian_date; + + // From: https://en.wikipedia.org/wiki/Julian_day + julian_day = (1461 * (year + 4800 + (month - 14)/12))/4 +(367 * (month - 2 - 12 * ((month - 14)/12)))/12 - (3 * ((year + 4900 + (month - 14)/12)/100))/4 + day - 32075; + julian_date = julian_day + (hours/24.0 - 0.5) + minutes/(24.0*60.0) + seconds/(24.0*60.0*60.0); + + return julian_date; +} + +// Calculate Julian date +double Astronomy::julianDate(QDateTime dt) +{ + QDateTime utc = dt.toUTC(); + QDate date = utc.date(); + QTime time = utc.time(); + return julianDate(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second()); +} + +// Get Julian date of J2000 Epoch +double Astronomy::jd_j2000(void) +{ + static double j2000 = 0.0; + + if (j2000 == 0.0) { + j2000 = julianDate(2000, 1, 1, 12, 0, 0); + } + return j2000; +} + +// Get Julian date of B1950 Epoch +double Astronomy::jd_b1950(void) +{ + static double b1950 = 0.0; + + if (b1950 == 0.0) { + b1950 = julianDate(1949, 12, 31, 22, 9, 0); + } + return b1950; +} + +// Get Julian date of current time Epoch +double Astronomy::jd_now(void) +{ + time_t system_time; + struct tm *utc_time; + + // Get current time in seconds since Unix Epoch (1970) + time(&system_time); + // Convert to UTC (GMT) + utc_time = gmtime(&system_time); + + // Convert to Julian date + return julianDate(utc_time->tm_year + 1900, utc_time->tm_mon + 1, utc_time->tm_mday, + utc_time->tm_hour, utc_time->tm_min, utc_time->tm_sec); +} + +// Precess a RA/DEC between two given Epochs +RADec Astronomy::precess(RADec rd_in, double jd_from, double jd_to) +{ + RADec rd_out; + double x, y, z; + double xp, yp, zp; + double ra_rad, dec_rad; + double rot[3][3]; // [row][col] + double ra_deg; + + double days_per_century = 36524.219878; + double t0 = (jd_from - jd_b1950())/days_per_century; // Tropical centuries since B1950.0 + double t = (jd_to - jd_from)/days_per_century; // Tropical centuries from starting epoch to ending epoch + + // From: https://www.cloudynights.com/topic/561254-ra-dec-epoch-conversion/ + rot[0][0] = 1.0 - ((29696.0 + 26.0*t0)*t*t - 13.0*t*t*t)*.00000001; + rot[1][0] = ((2234941.0 + 1355.0*t0)*t - 676.0*t*t + 221.0*t*t*t)*.00000001; + rot[2][0] = ((971690.0 - 414.0*t0)*t + 207.0*t*t + 96.0*t*t*t)*.00000001; + rot[0][1] = -rot[1][0]; + rot[1][1] = 1.0 - ((24975.0 + 30.0*t0)*t*t - 15.0*t*t*t)*.00000001; + rot[2][1] = -((10858.0 + 2.0*t0)*t*t)*.00000001; + rot[0][2] = -rot[2][0]; + rot[1][2] = rot[2][1]; + rot[2][2] = 1.0 - ((4721.0 - 4.0*t0)*t*t)*.00000001; + + // Hours to degrees + ra_deg = rd_in.ra*(360.0/24.0); + + // Convert to rectangular coordinates + ra_rad = Units::degreesToRadians(ra_deg); + dec_rad = Units::degreesToRadians(rd_in.dec); + x = cos(ra_rad) * cos(dec_rad); + y = sin(ra_rad) * cos(dec_rad); + z = sin(dec_rad); + + // Rotate + xp = rot[0][0]*x + rot[0][1]*y + rot[0][2]*z; + yp = rot[1][0]*x + rot[1][1]*y + rot[1][2]*z; + zp = rot[2][0]*x + rot[2][1]*y + rot[2][2]*z; + + // Convert back to spherical coordinates + rd_out.ra = Units::radiansToDegrees(atan(yp/xp)); + if (xp < 0.0) { + rd_out.ra += 180.0; + } else if ((yp < 0) && (xp > 0)) { + rd_out.ra += 360.0; + } + rd_out.dec = Units::radiansToDegrees(asin(zp)); + + // Degrees to hours + rd_out.ra /= (360.0/24.0); + + return rd_out; +} + +// Calculate local mean sidereal time (LMST) in degrees +double Astronomy::localSiderealTime(QDateTime dateTime, double longitude) +{ + double jd = julianDate(dateTime); + + double d = (jd - jd_j2000()); // Days since J2000 epoch (including fraction) + double f = fmod(jd, 1.0); // Fractional part is decimal days + double ut = (f+0.5)*24.0; // Universal time in decimal hours + + // https://astronomy.stackexchange.com/questions/24859/local-sidereal-time + // 100.46 is offset for GMST at 0h UT on 1 Jan 2000 + // 0.985647 is number of degrees per day over 360 the Earth rotates in one solar day + // Approx to 0.3 arcseconds + return fmod(100.46 + 0.985647 * d + longitude + (360/24) * ut, 360.0); +} + +// Convert from J2000 right ascension (decimal hours) and declination (decimal degrees) to altitude and azimuth, for location (decimal degrees) and time +AzAlt Astronomy::raDecToAzAlt(RADec rd, double latitude, double longitude, QDateTime dt, bool j2000) +{ + AzAlt aa; + double ra_deg; // Right ascension in degrees + double lst_deg; // Local sidereal time in degrees + double ha; // Hour angle + double a, az; + double dec_rad, lat_rad, ha_rad, alt_rad; // Corresponding variables as radians + + double jd = julianDate(dt); + + // Precess RA/DEC from J2000 Epoch to desired (typically current) Epoch + if (j2000) + rd = precess(rd, jd_j2000(), jd); + + // Calculate local mean sidereal time (LMST) in degrees + // https://astronomy.stackexchange.com/questions/24859/local-sidereal-time + // 100.46 is offset for GMST at 0h UT on 1 Jan 2000 + // 0.985647 is number of degrees per day over 360 the Earth rotates in one solar day + // Approx to 0.3 arcseconds + lst_deg = Astronomy::localSiderealTime(dt, longitude); + + // Convert right ascension from hours to degrees + ra_deg = rd.ra * (360.0/24.0); + + // Calculate hour angle + ha = fmod(lst_deg - ra_deg, 360.0); + + // Convert degrees to radians + dec_rad = Units::degreesToRadians(rd.dec); + lat_rad = Units::degreesToRadians(latitude); + ha_rad = Units::degreesToRadians(ha); + + // Calculate altitude and azimuth - no correction for atmospheric refraction + // From: http://www.convertalot.com/celestial_horizon_co-ordinates_calculator.html + alt_rad = asin(sin(dec_rad)*sin(lat_rad) + cos(dec_rad)*cos(lat_rad)*cos(ha_rad)); + a = Units::radiansToDegrees(acos((sin(dec_rad)-sin(alt_rad)*sin(lat_rad)) / (cos(alt_rad)*cos(lat_rad)))); + if (sin(ha_rad) < 0.0) { + az = a; + } else { + az = 360.0 - a; + } + + aa.alt = Units::radiansToDegrees(alt_rad); + aa.az = az; + return aa; +} + +// Needs to work for negative a +double Astronomy::modulo(double a, double b) +{ + return a - b * floor(a/b); +} + +// Calculate azimuth and altitude angles to the sun from the given latitude and longitude at the given time +// Refer to: +// https://en.wikipedia.org/wiki/Position_of_the_Sun +// https://www.aa.quae.nl/en/reken/zonpositie.html +// Said to be accurate to .01 degree (36") for dates between 1950 and 2050 +// although we use slightly more accurate constants and an extra term in the equation +// of centre from the second link +// For an alternative, see: http://www.psa.es/sdg/sunpos.htm +// Most accurate algorithm is supposedly: https://midcdmz.nrel.gov/spa/ +void Astronomy::sunPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt) +{ + double jd = julianDate(dt); + double n = (jd - jd_j2000()); // Days since J2000 epoch (including fraction) + + double l = 280.461 + 0.9856474 * n; // Mean longitude of the Sun, corrected for the abberation of light + double g = 357.5291 + 0.98560028 * n; // Mean anomaly of the Sun - how far around orbit from perihlion, in degrees + + l = modulo(l, 360.0); + g = modulo(g, 360.0); + + double gr = Units::degreesToRadians(g); + + double la = l + 1.9148 * sin(gr) + 0.0200 * sin(2.0*gr) + 0.0003 * sin(3.0*gr); // Ecliptic longitude (Ecliptic latitude b set to 0) + + // Convert la, b=0, which give the position of the Sun in the ecliptic coordinate sytem, to + // equatorial coordinates + + double e = 23.4393 - 3.563E-7 * n; // Obliquity of the ecliptic - tilt of Earth's axis of rotation + + double er = Units::degreesToRadians(e); + double lr = Units::degreesToRadians(la); + + double a = atan2(cos(er) * sin(lr), cos(lr)); // Right ascension, radians + double d = asin(sin(er) * sin(lr)); // Declination, radians + + rd.ra = modulo(Units::radiansToDegrees(a), 360.0) / (360.0/24.0); // Convert to hours + rd.dec = Units::radiansToDegrees(d); // Convert to degrees + + aa = raDecToAzAlt(rd, latitude, longitude, dt, false); +} + +double Astronomy::moonDays(QDateTime dt) +{ + QDateTime utc = dt.toUTC(); + QDate date = utc.date(); + QTime time = utc.time(); + + int y = date.year(); + int m = date.month(); + int D = date.day(); + int d = 367 * y - 7 * (y + (m+9)/12) / 4 - 3 * ((y + (m-9)/7) / 100 + 1) / 4 + 275*m/9 + D - 730515; + + return d + time.hour()/24.0 + time.minute()/(24.0*60.0) + time.second()/(24.0*60.0*60.0); +} + +// Calculate azimuth and altitude angles to the moon from the given latitude and longitude at the given time +// Refer to: https://stjarnhimlen.se/comp/ppcomp.html +// Accurate to 4 arcminute +void Astronomy::moonPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt) +{ + double d = moonDays(dt); + + double ecl = Units::degreesToRadians(23.4393 - 3.563E-7 * d); // Obliquity of the ecliptic - tilt of Earth's axis of rotation + + // Orbital elements for the Sun + double Ns = 0.0; + double is = 0.0; + double ws = Units::degreesToRadians(282.9404 + 4.70935E-5 * d); + double as = 1.0; // (AU) + double es = 0.016709 - 1.151E-9 * d; // ecs + double Ms = Units::degreesToRadians(356.0470 + 0.9856002585 * d); + + // Orbital elements for the Moon + double Nm = Units::degreesToRadians(125.1228 - 0.0529538083 * d); // longitude of the ascending node + double im = Units::degreesToRadians(5.1454); // inclination to the ecliptic (plane of the Earth's orbit) + double wm = Units::degreesToRadians(318.0634 + 0.1643573223 * d); // argument of perihelion + double am = 60.2666; // (Earth radii) semi-major axis, or mean distance from Sun + double em = 0.054900; // ecm // eccentricity (0=circle, 0-1=ellipse, 1=parabola) + double Mm = Units::degreesToRadians(115.3654 + 13.0649929509 * d); // mean anomaly (0 at perihelion; increases uniformly with time), degrees + + double Em = Mm + em * sin(Mm) * (1.0 + em * cos(Mm)); // Eccentric anomaly in radians + + double xv = am * (cos(Em) - em); + double yv = am * (sqrt(1.0 - em*em) * sin(Em)); + + double vm = atan2(yv, xv); // True anomaly + double rm = sqrt(xv*xv + yv*yv); // Distance + + // Compute position in space (for the Moon, this is geocentric) + double xh = rm * (cos(Nm) * cos(vm+wm) - sin(Nm) * sin(vm+wm) * cos(im)); + double yh = rm * (sin(Nm) * cos(vm+wm) + cos(Nm) * sin(vm+wm) * cos(im)); + double zh = rm * (sin(vm+wm) * sin(im)); + + // Convert to ecliptic longitude and latitude + double lonecl = atan2(yh, xh); + double latecl = atan2(zh, sqrt(xh*xh+yh*yh)); + + // Perturbations of the Moon + + double Ls = Ms + ws; // Mean Longitude of the Sun (Ns=0) + double Lm = Mm + wm + Nm; // Mean longitude of the Moon + double D = Lm - Ls; // Mean elongation of the Moon + double F = Lm - Nm; // Argument of latitude for the Moon + + double dlon; + dlon = -1.274 * sin(Mm - 2*D); // (the Evection) + dlon += +0.658 * sin(2*D); // (the Variation) + dlon += -0.186 * sin(Ms); // (the Yearly Equation) + dlon += -0.059 * sin(2*Mm - 2*D); + dlon += -0.057 * sin(Mm - 2*D + Ms); + dlon += +0.053 * sin(Mm + 2*D); + dlon += +0.046 * sin(2*D - Ms); + dlon += +0.041 * sin(Mm - Ms); + dlon += -0.035 * sin(D); // (the Parallactic Equation) + dlon += -0.031 * sin(Mm + Ms); + dlon += -0.015 * sin(2*F - 2*D); + dlon += +0.011 * sin(Mm - 4*D); + + double dlat; + dlat = -0.173 * sin(F - 2*D); + dlat += -0.055 * sin(Mm - F - 2*D); + dlat += -0.046 * sin(Mm + F - 2*D); + dlat += +0.033 * sin(F + 2*D); + dlat += +0.017 * sin(2*Mm + F); + + lonecl += Units::degreesToRadians(dlon); + latecl += Units::degreesToRadians(dlat); + + rm += -0.58 * cos(Mm - 2*D); + rm += -0.46 * cos(2*D); + + // Convert perturbed + xh = rm * cos(lonecl) * cos(latecl); + yh = rm * sin(lonecl) * cos(latecl); + zh = rm * sin(latecl); + + // Convert to geocentric coordinates (already the case for the Moon) + double xg = xh; + double yg = yh; + double zg = zh; + + // Convert to equatorial cordinates + double xe = xg; + double ye = yg * cos(ecl) - zg * sin(ecl); + double ze = yg * sin(ecl) + zg * cos(ecl); + + // Compute right ascension and declination + double ra = atan2(ye, xe); + double dec = atan2(ze, sqrt(xe*xe+ye*ye)); + + rd.ra = modulo(Units::radiansToDegrees(ra), 360.0) / (360.0/24.0); // Convert to hours + rd.dec = Units::radiansToDegrees(dec); // Convert to degrees + + // Convert from geocentric to topocentric + double mpar = asin(1/rm); + + double lat = Units::degreesToRadians(latitude); + double gclat = Units::degreesToRadians(latitude - 0.1924 * sin(2.0 * lat)); + double rho = 0.99833 + 0.00167 * cos(2*lat); + + QTime time = dt.toUTC().time(); + double UT = time.hour() + time.minute()/60.0 + time.second()/(60.0*60.0); + + double GMST0 = Units::radiansToDegrees(Ls)/15.0 + 12; // In hours + double GMST = GMST0 + UT; + double LST = GMST + longitude/15.0; + + double HA = Units::degreesToRadians(LST*15.0 - Units::radiansToDegrees(ra)); // Hour angle in radians + double g = atan(tan(gclat) / cos(HA)); + + double topRA = ra - mpar * rho * cos(gclat) * sin(HA) / cos(dec); + double topDec; + if (g != 0.0) + topDec = dec - mpar * rho * sin(gclat) * sin(g - dec) / sin(g); + else + topDec = dec - mpar * rho * sin(-dec) * cos(HA); + + rd.ra = modulo(Units::radiansToDegrees(topRA), 360.0) / (360.0/24.0); // Convert to hours + rd.dec = Units::radiansToDegrees(topDec); // Convert to degrees + aa = raDecToAzAlt(rd, latitude, longitude, dt, false); +} + +// Calculated adjustment to altitude angle from true to apparent due to atmospheric refraction using +// Saemundsson's formula (which is used by Stellarium and is primarily just for +// optical wavelengths) +// See: https://en.wikipedia.org/wiki/Atmospheric_refraction#Calculating_refraction +// Alt is in degrees. 90 = Zenith gives a factor of 0. +// Pressure in millibars +// Temperature in Celsuis +// We divide by 60.0 to get a value in degrees (as original formula is in arcminutes) +double Astronomy::refractionSaemundsson(double alt, double pressure, double temperature) +{ + double pt = (pressure/1010.0) * (283.0/(273.0+temperature)); + + return (1.02 / tan(Units::degreesToRadians(alt+10.3/(alt+5.11))) + 0.0019279) / 60.0; +} + +// Calculated adjustment to altitude angle from true to apparent due to atmospheric refraction using +// code from Starlink Positional Astronomy Library. This is more accurate for +// radio than Saemundsson's formula, but also more complex. +// See: https://github.com/Starlink/pal +// Alt is in degrees. 90 = Zenith gives a factor of 0. +// Pressure in millibars +// Temperature in Celsuis +// Humdity in % +// Frequency in Hertz +// Latitude in decimal degrees +// HeightAboveSeaLevel in metres +// Temperature lapse rate in K/km +double Astronomy::refractionPAL(double alt, double pressure, double temperature, double humidity, double frequency, + double latitude, double heightAboveSeaLevel, double temperatureLapseRate) +{ + double tdk = Units::celsiusToKelvin(temperature); // Ambient temperature at the observer (K) + double wl = (299792458.0/frequency)*1000000.0; // Wavelength in micrometres + double rh = humidity/100.0; // Relative humidity in range 0-1 + double phi = Units::degreesToRadians(latitude); // Latitude of the observer (radian, astronomical) + double tlr = temperatureLapseRate/1000.0; // Temperature lapse rate in the troposphere (K/metre) + double refa, refb; + double z = 90.0-alt; + double zu = Units::degreesToRadians(z); + double zr; + + palRefco(heightAboveSeaLevel, tdk, pressure, rh, + wl, phi, tlr, 1E-10, + &refa, &refb); + palRefz(zu, refa, refb, &zr); + + return z-Units::radiansToDegrees(zr); +} + +double Astronomy::raToDecimal(const QString& value) +{ + QRegExp decimal("^([0-9]+(\\.[0-9]+)?)"); + QRegExp hms("^([0-9]+)[ h]([0-9]+)[ m]([0-9]+(\\.[0-9]+)?)s?"); + + if (decimal.exactMatch(value)) + return decimal.capturedTexts()[0].toDouble(); + else if (hms.exactMatch(value)) + { + return Units::hoursMinutesSecondsToDecimal( + hms.capturedTexts()[1].toDouble(), + hms.capturedTexts()[2].toDouble(), + hms.capturedTexts()[3].toDouble()); + } + return 0.0; +} + +double Astronomy::decToDecimal(const QString& value) +{ + QRegExp decimal("^(-?[0-9]+(\\.[0-9]+)?)"); + QRegExp dms(QString("^(-?[0-9]+)[ %1d]([0-9]+)[ 'm]([0-9]+(\\.[0-9]+)?)[\"s]?").arg(QChar(0xb0))); + + if (decimal.exactMatch(value)) + return decimal.capturedTexts()[0].toDouble(); + else if (dms.exactMatch(value)) + { + return Units::degreesMinutesSecondsToDecimal( + dms.capturedTexts()[1].toDouble(), + dms.capturedTexts()[2].toDouble(), + dms.capturedTexts()[3].toDouble()); + } + return 0.0; +} + +double Astronomy::lstAndRAToLongitude(double lst, double raHours) +{ + double longitude = lst - (raHours * 15.0); // Convert hours to degrees + if (longitude < -180.0) + longitude += 360.0; + else if (longitude > 180.0) + longitude -= 360.0; + return -longitude; // East positive +} + +// The following functions are from Starlink Positional Astronomy Library +// https://github.com/Starlink/pal + +/* Pi */ +static const double PAL__DPI = 3.1415926535897932384626433832795028841971693993751; + +/* 2Pi */ +static const double PAL__D2PI = 6.2831853071795864769252867665590057683943387987502; + +/* pi/180: degrees to radians */ +static const double PAL__DD2R = 0.017453292519943295769236907684886127134428718885417; + +/* Radians to degrees */ +static const double PAL__DR2D = 57.295779513082320876798154814105170332405472466564; + +/* DMAX(A,B) - return maximum value - evaluates arguments multiple times */ +#define DMAX(A,B) ((A) > (B) ? (A) : (B) ) + +/* DMIN(A,B) - return minimum value - evaluates arguments multiple times */ +#define DMIN(A,B) ((A) < (B) ? (A) : (B) ) + +// Normalize angle into range +/- pi +double palDrange( double angle ) { + double result = fmod( angle, PAL__D2PI ); + if( result > PAL__DPI ) { + result -= PAL__D2PI; + } else if( result < -PAL__DPI ) { + result += PAL__D2PI; + } + return result; +} + +// Calculate stratosphere parameters +static void pal1Atms ( double rt, double tt, double dnt, double gamal, + double r, double * dn, double * rdndr ) { + + double b; + double w; + + b = gamal / tt; + w = (dnt - 1.0) * exp( -b * (r-rt) ); + *dn = 1.0 + w; + *rdndr = -r * b * w; + +} + +// Calculate troposphere parameters +static void pal1Atmt ( double r0, double t0, double alpha, double gamm2, + double delm2, double c1, double c2, double c3, double c4, + double c5, double c6, double r, + double *t, double *dn, double *rdndr ) { + + double tt0; + double tt0gm2; + double tt0dm2; + + *t = DMAX( DMIN( t0 - alpha*(r-r0), 320.0), 100.0 ); + tt0 = *t / t0; + tt0gm2 = pow( tt0, gamm2 ); + tt0dm2 = pow( tt0, delm2 ); + *dn = 1.0 + ( c1 * tt0gm2 - ( c2 - c5 / *t ) * tt0dm2 ) * tt0; + *rdndr = r * ( -c3 * tt0gm2 + ( c4 - c6 / tt0 ) * tt0dm2 ); + +} + +// Adjust unrefracted zenith distance +static void palRefz ( double zu, double refa, double refb, double *zr ) { + + /* Constants */ + + /* Largest usable ZD (deg) */ + const double D93 = 93.0; + + /* ZD at which one model hands over to the other (radians) */ + const double Z83 = 83.0 * PAL__DD2R; + + /* coefficients for high ZD model (used beyond ZD 83 deg) */ + const double C1 = +0.55445; + const double C2 = -0.01133; + const double C3 = +0.00202; + const double C4 = +0.28385; + const double C5 = +0.02390; + + /* High-ZD-model prefiction (deg) for that point */ + const double REF83 = (C1+C2*7.0+C3*49.0)/(1.0+C4*7.0+C5*49.0); + + double zu1,zl,s,c,t,tsq,tcu,ref,e,e2; + + /* perform calculations for zu or 83 deg, whichever is smaller */ + zu1 = DMIN(zu,Z83); + + /* functions of ZD */ + zl = zu1; + s = sin(zl); + c = cos(zl); + t = s/c; + tsq = t*t; + tcu = t*tsq; + + /* refracted zd (mathematically to better than 1 mas at 70 deg) */ + zl = zl-(refa*t+refb*tcu)/(1.0+(refa+3.0*refb*tsq)/(c*c)); + + /* further iteration */ + s = sin(zl); + c = cos(zl); + t = s/c; + tsq = t*t; + tcu = t*tsq; + ref = zu1-zl+ + (zl-zu1+refa*t+refb*tcu)/(1.0+(refa+3.0*refb*tsq)/(c*c)); + + /* special handling for large zu */ + if (zu > zu1) { + e = 90.0-DMIN(D93,zu*PAL__DR2D); + e2 = e*e; + ref = (ref/REF83)*(C1+C2*e+C3*e2)/(1.0+C4*e+C5*e2); + } + + /* return refracted zd */ + *zr = zu-ref; + +} + +// Determine constants in atmospheric refraction model +static void palRefco ( double hm, double tdk, double pmb, double rh, + double wl, double phi, double tlr, double eps, + double *refa, double *refb ) { + + double r1, r2; + + /* Sample zenith distances: arctan(1) and arctan(4) */ + const double ATN1 = 0.7853981633974483; + const double ATN4 = 1.325817663668033; + + /* Determine refraction for the two sample zenith distances */ + palRefro(ATN1,hm,tdk,pmb,rh,wl,phi,tlr,eps,&r1); + palRefro(ATN4,hm,tdk,pmb,rh,wl,phi,tlr,eps,&r2); + + /* Solve for refraction constants */ + *refa = (64.0*r1-r2)/60.0; + *refb = (r2-4.0*r1)/60.0; + +} + +// Atmospheric refraction for radio and optical/IR wavelengths +static void palRefro( double zobs, double hm, double tdk, double pmb, + double rh, double wl, double phi, double tlr, + double eps, double * ref ) { + + /* + * Fixed parameters + */ + + /* 93 degrees in radians */ + const double D93 = 1.623156204; + /* Universal gas constant */ + const double GCR = 8314.32; + /* Molecular weight of dry air */ + const double DMD = 28.9644; + /* Molecular weight of water vapour */ + const double DMW = 18.0152; + /* Mean Earth radius (metre) */ + const double S = 6378120.; + /* Exponent of temperature dependence of water vapour pressure */ + const double DELTA = 18.36; + /* Height of tropopause (metre) */ + const double HT = 11000.; + /* Upper limit for refractive effects (metre) */ + const double HS = 80000.; + /* Numerical integration: maximum number of strips. */ + const int ISMAX=16384l; + + /* Local variables */ + int is, k, n, i, j; + + int optic, loop; /* booleans */ + + double zobs1,zobs2,hmok,tdkok,pmbok,rhok,wlok,alpha, + tol,wlsq,gb,a,gamal,gamma,gamm2,delm2, + tdc,psat,pwo,w, + c1,c2,c3,c4,c5,c6,r0,tempo,dn0,rdndr0,sk0,f0, + rt,tt,dnt,rdndrt,sine,zt,ft,dnts,rdndrp,zts,fts, + rs,dns,rdndrs,zs,fs,refold,z0,zrange,fb,ff,fo,fe, + h,r,sz,rg,dr,tg,dn,rdndr,t,f,refp,reft; + + /* The refraction integrand */ +#define refi(DN,RDNDR) RDNDR/(DN+RDNDR) + + /* Transform ZOBS into the normal range. */ + zobs1 = palDrange(zobs); + zobs2 = DMIN(fabs(zobs1),D93); + + /* keep other arguments within safe bounds. */ + hmok = DMIN(DMAX(hm,-1e3),HS); + tdkok = DMIN(DMAX(tdk,100.0),500.0); + pmbok = DMIN(DMAX(pmb,0.0),10000.0); + rhok = DMIN(DMAX(rh,0.0),1.0); + wlok = DMAX(wl,0.1); + alpha = DMIN(DMAX(fabs(tlr),0.001),0.01); + + /* tolerance for iteration. */ + tol = DMIN(DMAX(fabs(eps),1e-12),0.1)/2.0; + + /* decide whether optical/ir or radio case - switch at 100 microns. */ + optic = wlok < 100.0; + + /* set up model atmosphere parameters defined at the observer. */ + wlsq = wlok*wlok; + gb = 9.784*(1.0-0.0026*cos(phi+phi)-0.00000028*hmok); + if (optic) { + a = (287.6155+(1.62887+0.01360/wlsq)/wlsq) * 273.15e-6/1013.25; + } else { + a = 77.6890e-6; + } + gamal = (gb*DMD)/GCR; + gamma = gamal/alpha; + gamm2 = gamma-2.0; + delm2 = DELTA-2.0; + tdc = tdkok-273.15; + psat = pow(10.0,(0.7859+0.03477*tdc)/(1.0+0.00412*tdc)) * + (1.0+pmbok*(4.5e-6+6.0e-10*tdc*tdc)); + if (pmbok > 0.0) { + pwo = rhok*psat/(1.0-(1.0-rhok)*psat/pmbok); + } else { + pwo = 0.0; + } + w = pwo*(1.0-DMW/DMD)*gamma/(DELTA-gamma); + c1 = a*(pmbok+w)/tdkok; + if (optic) { + c2 = (a*w+11.2684e-6*pwo)/tdkok; + } else { + c2 = (a*w+6.3938e-6*pwo)/tdkok; + } + c3 = (gamma-1.0)*alpha*c1/tdkok; + c4 = (DELTA-1.0)*alpha*c2/tdkok; + if (optic) { + c5 = 0.0; + c6 = 0.0; + } else { + c5 = 375463e-6*pwo/tdkok; + c6 = c5*delm2*alpha/(tdkok*tdkok); + } + + /* conditions at the observer. */ + r0 = S+hmok; + pal1Atmt(r0,tdkok,alpha,gamm2,delm2,c1,c2,c3,c4,c5,c6, + r0,&tempo,&dn0,&rdndr0); + sk0 = dn0*r0*sin(zobs2); + f0 = refi(dn0,rdndr0); + + /* conditions in the troposphere at the tropopause. */ + rt = S+DMAX(HT,hmok); + pal1Atmt(r0,tdkok,alpha,gamm2,delm2,c1,c2,c3,c4,c5,c6, + rt,&tt,&dnt,&rdndrt); + sine = sk0/(rt*dnt); + zt = atan2(sine,sqrt(DMAX(1.0-sine*sine,0.0))); + ft = refi(dnt,rdndrt); + + /* conditions in the stratosphere at the tropopause. */ + pal1Atms(rt,tt,dnt,gamal,rt,&dnts,&rdndrp); + sine = sk0/(rt*dnts); + zts = atan2(sine,sqrt(DMAX(1.0-sine*sine,0.0))); + fts = refi(dnts,rdndrp); + + /* conditions at the stratosphere limit. */ + rs = S+HS; + pal1Atms(rt,tt,dnt,gamal,rs,&dns,&rdndrs); + sine = sk0/(rs*dns); + zs = atan2(sine,sqrt(DMAX(1.0-sine*sine,0.0))); + fs = refi(dns,rdndrs); + + /* variable initialization to avoid compiler warning. */ + reft = 0.0; + + /* integrate the refraction integral in two parts; first in the + * troposphere (k=1), then in the stratosphere (k=2). */ + + for (k=1; k<=2; k++) { + + /* initialize previous refraction to ensure at least two iterations. */ + refold = 1.0; + + /* start off with 8 strips. */ + is = 8; + + /* start z, z range, and start and end values. */ + if (k==1) { + z0 = zobs2; + zrange = zt-z0; + fb = f0; + ff = ft; + } else { + z0 = zts; + zrange = zs-z0; + fb = fts; + ff = fs; + } + + /* sums of odd and even values. */ + fo = 0.0; + fe = 0.0; + + /* first time through the loop we have to do every point. */ + n = 1; + + /* start of iteration loop (terminates at specified precision). */ + loop = 1; + while (loop) { + + /* strip width. */ + h = zrange/((double)is); + + /* initialize distance from earth centre for quadrature pass. */ + if (k == 1) { + r = r0; + } else { + r = rt; + } + + /* one pass (no need to compute evens after first time). */ + for (i=1; i 1e-20) { + w = sk0/sz; + rg = r; + dr = 1.0e6; + j = 0; + while ( fabs(dr) > 1.0 && j < 4 ) { + j++; + if (k==1) { + pal1Atmt(r0,tdkok,alpha,gamm2,delm2, + c1,c2,c3,c4,c5,c6,rg,&tg,&dn,&rdndr); + } else { + pal1Atms(rt,tt,dnt,gamal,rg,&dn,&rdndr); + } + dr = (rg*dn-w)/(dn+rdndr); + rg = rg-dr; + } + r = rg; + } + + /* find the refractive index and integrand at r. */ + if (k==1) { + pal1Atmt(r0,tdkok,alpha,gamm2,delm2, + c1,c2,c3,c4,c5,c6,r,&t,&dn,&rdndr); + } else { + pal1Atms(rt,tt,dnt,gamal,r,&dn,&rdndr); + } + f = refi(dn,rdndr); + + /* accumulate odd and (first time only) even values. */ + if (n==1 && i%2 == 0) { + fe += f; + } else { + fo += f; + } + } + + /* evaluate the integrand using simpson's rule. */ + refp = h*(fb+4.0*fo+2.0*fe+ff)/3.0; + + /* has the required precision been achieved (or can't be)? */ + if (fabs(refp-refold) > tol && is < ISMAX) { + + /* no: prepare for next iteration.*/ + + /* save current value for convergence test. */ + refold = refp; + + /* double the number of strips. */ + is += is; + + /* sum of all current values = sum of next pass's even values. */ + fe += fo; + + /* prepare for new odd values. */ + fo = 0.0; + + /* skip even values next time. */ + n = 2; + } else { + + /* yes: save troposphere component and terminate the loop. */ + if (k==1) reft = refp; + loop = 0; + } + } + } + + /* result. */ + *ref = reft+refp; + if (zobs1 < 0.0) *ref = -(*ref); + +} diff --git a/sdrbase/util/astronomy.h b/sdrbase/util/astronomy.h new file mode 100644 index 000000000..3784c8196 --- /dev/null +++ b/sdrbase/util/astronomy.h @@ -0,0 +1,68 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ASTRONOMY_H +#define INCLUDE_ASTRONOMY_H + +#include "export.h" + +class QDateTime; + +// Right ascension and declination +struct SDRBASE_API RADec { + double ra; + double dec; +}; + +// Azimuth and Altitude +struct SDRBASE_API AzAlt { + double az; + double alt; +}; + +class SDRBASE_API Astronomy { + +public: + static double julianDate(int year, int month, int day, int hours, int minutes, int seconds); + static double julianDate(QDateTime dt); + + static double jd_j2000(void); + static double jd_b1950(void); + static double jd_now(void); + + static RADec precess(RADec rd_in, double jd_from, double jd_to); + static AzAlt raDecToAzAlt(RADec rd, double latitude, double longitude, QDateTime dt, bool j2000=true); + + static double localSiderealTime(QDateTime dateTime, double longitude); + + static void sunPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt); + static double moonDays(QDateTime dt); + static void moonPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt); + + static double refractionSaemundsson(double alt, double pressure, double temperature); + static double refractionPAL(double alt, double pressure, double temperature, double humidity, double frequency, double latitude, double heightAboveSeaLevel, double temperatureLapseRate); + + static double raToDecimal(const QString& value); + static double decToDecimal(const QString& value); + + static double lstAndRAToLongitude(double lst, double raHours); + +protected: + static double modulo(double a, double b); +}; + +#endif // INCLUDE_ASTRONOMY_H