diff --git a/CMakeLists.txt b/CMakeLists.txt index 7642e3a40..ce5e14658 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -238,6 +238,9 @@ elseif (WIN32) set(UHD_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/uhd/lib/uhd.lib" CACHE INTERNAL "") set(UHD_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/uhd/bin" CACHE INTERNAL "") + set(OPENSSL_FOUND ON CACHE INTERNAL "") + set(OPENSSL_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/openssl" CACHE INTERNAL "") + # ffmpeg set(FFMPEG_INCLUDE_DIRS "${EXTERNAL_LIBRARY_FOLDER}/ffmpeg/include" CACHE INTERNAL "") set(FFMPEG_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/ffmpeg/bin" CACHE INTERNAL "") @@ -331,11 +334,15 @@ find_package(Qt5 COMPONENTS Widgets REQUIRED) find_package(Qt5 COMPONENTS WebSockets REQUIRED) find_package(Qt5 COMPONENTS Multimedia REQUIRED) find_package(Qt5 COMPONENTS MultimediaWidgets REQUIRED) +find_package(Qt5 COMPONENTS SerialPort) -# for the server we don't need OpenGL components +# for the server we don't need OpenGL/Qt Quick components if (BUILD_GUI) find_package(OpenGL REQUIRED) find_package(Qt5 COMPONENTS OpenGL REQUIRED) + find_package(Qt5 COMPONENTS Quick) + find_package(Qt5 COMPONENTS QuickWidgets) + find_package(Qt5 COMPONENTS Positioning) endif() # other requirements @@ -589,6 +596,10 @@ endif() if (LINUX AND ENABLE_EXTERNAL_LIBRARIES) install(DIRECTORY ${EXTERNAL_BUILD_LIBRARIES}/lib/ DESTINATION ${INSTALL_LIB_DIR}) endif() +if(WIN32) + include(DeployQt) + windeployqt(${CMAKE_PROJECT_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/sdrgui/resources) +endif() # install documentation # TODO maybe install readme for every plugins diff --git a/cmake/Modules/DeployQt.cmake b/cmake/Modules/DeployQt.cmake new file mode 100644 index 000000000..d8a1d2d79 --- /dev/null +++ b/cmake/Modules/DeployQt.cmake @@ -0,0 +1,49 @@ +find_package(Qt5Core REQUIRED) + +get_target_property(_qmake_executable Qt5::qmake IMPORTED_LOCATION) +get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) + +find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${_qt_bin_dir}") +if(WIN32 AND NOT WINDEPLOYQT_EXECUTABLE) + message(FATAL_ERROR "windeployqt not found") +endif() + +# Add commands that copy the required Qt files to ${bindir} as well as including +# them in final installation (by first copying them to a winqt subdir) +# We need to specify ${bindir} as we run this on plugins as well as the main .exe +# Preferably, it would be nicer to skip the extra copy to winqt subdir, but how? +# Also, we should possibly only call install once, after all deployments are made +function(windeployqt target bindir qmldir) + + # Run windeployqt after build + # First deploy in to bin directory, so we can run from the build bin directory + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E + env PATH="${_qt_bin_dir}" "${WINDEPLOYQT_EXECUTABLE}" + --verbose 1 + --no-compiler-runtime + --dir "${bindir}" + --qmldir "${qmldir}" + --multimedia + \"$\" + COMMENT "Deploying Qt..." + ) + + # Then, deploy again in to separate directory for install to pick up + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E + env PATH="${_qt_bin_dir}" "${WINDEPLOYQT_EXECUTABLE}" + --verbose 1 + --no-compiler-runtime + --dir "${bindir}/winqt" + --qmldir "${qmldir}" + --multimedia + \"$\" + COMMENT "Deploying Qt..." + ) + + install(DIRECTORY "${bindir}/winqt/" DESTINATION .) + +endfunction() + +mark_as_advanced(WINDEPLOYQT_EXECUTABLE) diff --git a/cmake/cpack/CMakeLists.txt b/cmake/cpack/CMakeLists.txt index c8c80f70a..259853eeb 100644 --- a/cmake/cpack/CMakeLists.txt +++ b/cmake/cpack/CMakeLists.txt @@ -11,6 +11,7 @@ endfunction (QUERY_QMAKE) query_qmake (QT_INSTALL_PLUGINS QT_PLUGINS_DIR) query_qmake (QT_INSTALL_IMPORTS QT_IMPORTS_DIR) +query_qmake (QT_INSTALL_QML QT_QML_DIR) if(APPLE AND BUNDLE AND BUILD_GUI) @@ -172,24 +173,6 @@ elseif(LINUX AND BUNDLE) elseif(WIN32 OR MINGW) - set(QT_PLUGINS_DIR_SET - "${QT_PLUGINS_DIR}/platforms" - "${QT_PLUGINS_DIR}/audio" - "${QT_PLUGINS_DIR}/imageformats" - "${QT_PLUGINS_DIR}/mediaservice" - "${QT_PLUGINS_DIR}/playlistformats" - "${QT_PLUGINS_DIR}/renderplugins" - "${QT_PLUGINS_DIR}/iconengines" - ) - - # Copy Qt Plugins; fixup_bundle doesn't do that - foreach(qt_plugin ${QT_PLUGINS_DIR_SET}) - get_filename_component(qt_plugin_name "${qt_plugin}" NAME) - add_custom_target(copy_qt_plugin_${qt_plugin_name} ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory "${qt_plugin}" "${SDRANGEL_BINARY_BIN_DIR}/${qt_plugin_name}" - ) - endforeach(qt_plugin) - # unfortunately some libraries are dependencies of dll so we copy by hand # TODO check if we can use fixup_bundle() and have libraries on root path @@ -242,6 +225,14 @@ elseif(WIN32 OR MINGW) endforeach(uhd_dll) endif() + file(GLOB OPENSSL_DLLS "${OPENSSL_DLL_DIR}/*${CMAKE_SHARED_LIBRARY_SUFFIX}") + foreach(openssl_dll ${OPENSSL_DLLS}) + get_filename_component(openssl_dll_name "${openssl_dll}" NAME) + add_custom_target(copy_openssl_${openssl_dll_name} ALL + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${openssl_dll}" "${SDRANGEL_BINARY_BIN_DIR}/" + ) + endforeach(openssl_dll) + # TODO we need a way to fixup_bundle() on the build bin/ directory without call install if(BUILD_GUI) install(CODE " @@ -268,23 +259,9 @@ elseif(WIN32 OR MINGW) " COMPONENT Runtime) endif(BUILD_GUI) - # Copy Qt Plugins; fixup_bundle doesn't do that - install ( - DIRECTORY ${QT_PLUGINS_DIR_SET} - DESTINATION "${INSTALL_BIN_DIR}" - CONFIGURATIONS Release MinSizeRel - COMPONENT runtime - FILES_MATCHING PATTERN "*${CMAKE_SHARED_LIBRARY_SUFFIX}" - PATTERN "*minimal*${CMAKE_SHARED_LIBRARY_SUFFIX}" EXCLUDE - PATTERN "*offscreen*${CMAKE_SHARED_LIBRARY_SUFFIX}" EXCLUDE - PATTERN "*quick*${CMAKE_SHARED_LIBRARY_SUFFIX}" EXCLUDE - PATTERN "*_debug${CMAKE_SHARED_LIBRARY_SUFFIX}" EXCLUDE - ) - # unfortunately some libraries are dependencies of dll so we copy by hand # TODO check if we can use fixup_bundle() and have libraries on root path # inputkiwisdr.dll - install(FILES "${Qt5_DIR}/../../../bin/Qt5WebSockets${CMAKE_SHARED_LIBRARY_SUFFIX}" DESTINATION "${INSTALL_LIB_DIR}") # demoddatv.dll install(DIRECTORY "${FFMPEG_LIBRARIES}/" DESTINATION "${INSTALL_LIB_DIR}" FILES_MATCHING PATTERN "*${CMAKE_SHARED_LIBRARY_SUFFIX}") @@ -300,6 +277,9 @@ elseif(WIN32 OR MINGW) # uhd install(DIRECTORY "${UHD_DLL_DIR}/" DESTINATION "${INSTALL_LIB_DIR}" FILES_MATCHING PATTERN "*${CMAKE_SHARED_LIBRARY_SUFFIX}") + # OpenSSL + install(DIRECTORY "${OPENSSL_DLL_DIR}/" DESTINATION "${INSTALL_LIB_DIR}" + FILES_MATCHING PATTERN "*${CMAKE_SHARED_LIBRARY_SUFFIX}") install(CODE " # remove *.lib files diff --git a/doc/img/ADSBDemod_plugin.png b/doc/img/ADSBDemod_plugin.png new file mode 100644 index 000000000..ec76e6ee1 Binary files /dev/null and b/doc/img/ADSBDemod_plugin.png differ diff --git a/doc/img/ADSBDemod_plugin.xcf b/doc/img/ADSBDemod_plugin.xcf new file mode 100644 index 000000000..e60cae85f Binary files /dev/null and b/doc/img/ADSBDemod_plugin.xcf differ diff --git a/doc/img/ADSBDemod_plugin_map.png b/doc/img/ADSBDemod_plugin_map.png new file mode 100644 index 000000000..645b5bfbc Binary files /dev/null and b/doc/img/ADSBDemod_plugin_map.png differ diff --git a/doc/img/ADSBDemod_plugin_table.png b/doc/img/ADSBDemod_plugin_table.png new file mode 100644 index 000000000..97d70921d Binary files /dev/null and b/doc/img/ADSBDemod_plugin_table.png differ diff --git a/doc/img/GS232Controller_plugin.png b/doc/img/GS232Controller_plugin.png new file mode 100644 index 000000000..2d4acd67d Binary files /dev/null and b/doc/img/GS232Controller_plugin.png differ diff --git a/doc/img/GS232Controller_plugin.xcf b/doc/img/GS232Controller_plugin.xcf new file mode 100644 index 000000000..8ca672e50 Binary files /dev/null and b/doc/img/GS232Controller_plugin.xcf differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 322fb6961..dcdb70bd4 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -1,5 +1,8 @@ project(demod) +if (Qt5Quick_FOUND AND Qt5QuickWidgets_FOUND AND Qt5Positioning_FOUND) + add_subdirectory(demodadsb) +endif() add_subdirectory(demodam) add_subdirectory(demodbfm) add_subdirectory(demodnfm) diff --git a/plugins/channelrx/demodadsb/CMakeLists.txt b/plugins/channelrx/demodadsb/CMakeLists.txt new file mode 100644 index 000000000..4cff3eb67 --- /dev/null +++ b/plugins/channelrx/demodadsb/CMakeLists.txt @@ -0,0 +1,70 @@ +project(adsb) + +set(adsb_SOURCES + adsbdemod.cpp + adsbdemodsettings.cpp + adsbdemodwebapiadapter.cpp + adsbplugin.cpp + adsbdemodsink.cpp + adsbdemodbaseband.cpp + adsbdemodreport.cpp + adsbdemodworker.cpp +) + +set(adsb_HEADERS + adsbdemod.h + adsbdemodsettings.h + adsbdemodwebapiadapter.h + adsbplugin.h + adsbdemodsink.h + adsbdemodbaseband.h + adsbdemodreport.h + adsbdemodworker.h + adsb.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(adsb_SOURCES + ${adsb_SOURCES} + adsbdemodgui.cpp + adsbdemodgui.ui + ) + set(adsb_HEADERS + ${adsb_HEADERS} + adsbdemodgui.h + ) + + set(TARGET_NAME demodadsb) + set(TARGET_LIB Qt5::Widgets Qt5::Quick Qt5::QuickWidgets Qt5::Positioning) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodadsbsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${adsb_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + # Run deployqt for QtQuick etc + include(DeployQt) + windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/../../../sdrgui/resources) +endif() diff --git a/plugins/channelrx/demodadsb/adsb.h b/plugins/channelrx/demodadsb/adsb.h new file mode 100644 index 000000000..11b47faee --- /dev/null +++ b/plugins/channelrx/demodadsb/adsb.h @@ -0,0 +1,29 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_ADSB_H +#define INCLUDE_ADSB_H + +#define ADS_B_CHIPS_PER_BIT 2 +#define ADS_B_PREAMBLE_BITS 8 +#define ADS_B_PREAMBLE_CHIPS (ADS_B_PREAMBLE_BITS*ADS_B_CHIPS_PER_BIT) +#define ADS_B_ES_BITS 112 +#define ADS_B_ES_BYTES (ADS_B_ES_BITS/8) +#define ADS_B_DF_MASK 0x1f +#define ADS_B_BITS_PER_SECOND 1000000 + +#endif // INCLUDE_ADSB_H diff --git a/plugins/channelrx/demodadsb/adsbdemod.cpp b/plugins/channelrx/demodadsb/adsbdemod.cpp new file mode 100644 index 000000000..46e4e46b7 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemod.cpp @@ -0,0 +1,468 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGADSBDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGADSBDemodReport.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/devicesamplemimo.h" +#include "device/deviceapi.h" +#include "util/db.h" + +#include "adsbdemod.h" +#include "adsbdemodworker.h" + +MESSAGE_CLASS_DEFINITION(ADSBDemod::MsgConfigureADSBDemod, Message) + +const QString ADSBDemod::m_channelIdURI = "sdrangel.channel.adsbdemod"; +const QString ADSBDemod::m_channelId = "ADSBDemod"; + +ADSBDemod::ADSBDemod(DeviceAPI *devieAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(devieAPI), + m_basebandSampleRate(0), + m_targetAzElValid(false), + m_targetAzimuth(0.0f), + m_targetElevation(0.0f) +{ + qDebug("ADSBDemod::ADSBDemod"); + setObjectName(m_channelId); + + m_thread = new QThread(this); + m_basebandSink = new ADSBDemodBaseband(); + m_basebandSink->moveToThread(m_thread); + + m_worker = new ADSBDemodWorker(); + m_basebandSink->setMessageQueueToWorker(m_worker->getInputMessageQueue()); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +ADSBDemod::~ADSBDemod() +{ + if (m_worker->isRunning()) { + stop(); + } + delete m_worker; + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + delete m_basebandSink; + delete m_thread; +} + +uint32_t ADSBDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void ADSBDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void ADSBDemod::start() +{ + qDebug() << "ADSBDemod::start"; + + if (m_basebandSampleRate != 0) { + m_basebandSink->setBasebandSampleRate(m_basebandSampleRate); + } + + m_worker->reset(); + m_worker->startWork(); + m_basebandSink->reset(); + m_thread->start(); + + ADSBDemodWorker::MsgConfigureADSBDemodWorker *msg = ADSBDemodWorker::MsgConfigureADSBDemodWorker::create(m_settings, true); + m_worker->getInputMessageQueue()->push(msg); +} + +void ADSBDemod::stop() +{ + qDebug() << "ADSBDemod::stop"; + m_worker->stopWork(); + m_thread->exit(); + m_thread->wait(); +} + +bool ADSBDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureADSBDemod::match(cmd)) + { + MsgConfigureADSBDemod& cfg = (MsgConfigureADSBDemod&) cmd; + qDebug() << "ADSBDemod::handleMessage: MsgConfigureADSBDemod"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "ADSBDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + + return true; + } + else + { + return false; + } +} + +void ADSBDemod::applySettings(const ADSBDemodSettings& settings, bool force) +{ + qDebug() << "ADSBDemod::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_streamIndex: " << settings.m_streamIndex + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_rgbColor != m_settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + if ((settings.m_title != m_settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSink(this, settings.m_streamIndex); + m_deviceAPI->addChannelSinkAPI(this); + } + + reverseAPIKeys.append("streamIndex"); + } + + ADSBDemodBaseband::MsgConfigureADSBDemodBaseband *msg = ADSBDemodBaseband::MsgConfigureADSBDemodBaseband::create(settings, force); + m_basebandSink->getInputMessageQueue()->push(msg); + + ADSBDemodWorker::MsgConfigureADSBDemodWorker *workerMsg = ADSBDemodWorker::MsgConfigureADSBDemodWorker::create(settings, force); + m_worker->getInputMessageQueue()->push(workerMsg); + + 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_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +QByteArray ADSBDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool ADSBDemod::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureADSBDemod *msg = MsgConfigureADSBDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + + return success; +} + +int ADSBDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAdsbDemodSettings(new SWGSDRangel::SWGADSBDemodSettings()); + response.getAdsbDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int ADSBDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + ADSBDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureADSBDemod *msg = MsgConfigureADSBDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureADSBDemod *msgToGUI = MsgConfigureADSBDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void ADSBDemod::webapiUpdateChannelSettings( + ADSBDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getAdsbDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getAdsbDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("correlationThreshold")) { + settings.m_correlationThreshold = response.getAdsbDemodSettings()->getCorrelationThreshold(); + } + if (channelSettingsKeys.contains("samplesPerBit")) { + settings.m_samplesPerBit = response.getAdsbDemodSettings()->getSamplesPerBit(); + } + if (channelSettingsKeys.contains("removeTimeout")) { + settings.m_removeTimeout = response.getAdsbDemodSettings()->getRemoveTimeout(); + } + if (channelSettingsKeys.contains("beastEnabled")) { + settings.m_beastEnabled = response.getAdsbDemodSettings()->getBeastEnabled() != 0; + } + if (channelSettingsKeys.contains("beastHost")) { + settings.m_beastHost = *response.getAdsbDemodSettings()->getBeastHost(); + } + if (channelSettingsKeys.contains("beastPort")) { + settings.m_beastPort = response.getAdsbDemodSettings()->getBeastPort(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getAdsbDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getAdsbDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getAdsbDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAdsbDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAdsbDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAdsbDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getAdsbDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getAdsbDemodSettings()->getReverseApiChannelIndex(); + } +} + +int ADSBDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAdsbDemodReport(new SWGSDRangel::SWGADSBDemodReport()); + response.getAdsbDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void ADSBDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const ADSBDemodSettings& settings) +{ + response.getAdsbDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getAdsbDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getAdsbDemodSettings()->setCorrelationThreshold(settings.m_correlationThreshold); + response.getAdsbDemodSettings()->setSamplesPerBit(settings.m_samplesPerBit); + response.getAdsbDemodSettings()->setRemoveTimeout(settings.m_removeTimeout); + response.getAdsbDemodSettings()->setBeastEnabled(settings.m_beastEnabled ? 1 : 0); + response.getAdsbDemodSettings()->setBeastHost(new QString(settings.m_beastHost)); + response.getAdsbDemodSettings()->setBeastPort(settings.m_beastPort); + response.getAdsbDemodSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getAdsbDemodSettings()->getTitle()) { + *response.getAdsbDemodSettings()->getTitle() = settings.m_title; + } else { + response.getAdsbDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getAdsbDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getAdsbDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAdsbDemodSettings()->getReverseApiAddress()) { + *response.getAdsbDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAdsbDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAdsbDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAdsbDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getAdsbDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); +} + +void ADSBDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + + response.getAdsbDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg)); + response.getAdsbDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate()); + if (m_targetAzElValid) + { + response.getAdsbDemodReport()->setTargetAzimuth(m_targetAzimuth); + response.getAdsbDemodReport()->setTargetElevation(m_targetElevation); + } +} + +void ADSBDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const ADSBDemodSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + swgChannelSettings->setDirection(0); // single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("ADSBDemod")); + swgChannelSettings->setAdsbDemodSettings(new SWGSDRangel::SWGADSBDemodSettings()); + SWGSDRangel::SWGADSBDemodSettings *swgADSBDemodSettings = swgChannelSettings->getAdsbDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgADSBDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgADSBDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("correlationThreshold") || force) { + swgADSBDemodSettings->setCorrelationThreshold(settings.m_correlationThreshold); + } + if (channelSettingsKeys.contains("samplesPerBit") || force) { + swgADSBDemodSettings->setSamplesPerBit(settings.m_samplesPerBit); + } + if (channelSettingsKeys.contains("removeTimeout") || force) { + swgADSBDemodSettings->setRemoveTimeout(settings.m_removeTimeout); + } + if (channelSettingsKeys.contains("beastEnabled") || force) { + swgADSBDemodSettings->setBeastEnabled(settings.m_beastEnabled ? 1 : 0); + } + if (channelSettingsKeys.contains("beastHost") || force) { + swgADSBDemodSettings->setBeastHost(new QString(settings.m_beastHost)); + } + if (channelSettingsKeys.contains("beastPort") || force) { + swgADSBDemodSettings->setBeastPort(settings.m_beastPort); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgADSBDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgADSBDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgADSBDemodSettings->setStreamIndex(settings.m_streamIndex); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->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 swgChannelSettings; +} + +void ADSBDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "ADSBDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("ADSBDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/channelrx/demodadsb/adsbdemod.h b/plugins/channelrx/demodadsb/adsbdemod.h new file mode 100644 index 000000000..8d9c5e6af --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemod.h @@ -0,0 +1,155 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2020 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_ADSBDEMOD_H +#define INCLUDE_ADSBDEMOD_H + +#include + +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "adsbdemodbaseband.h" +#include "adsbdemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ADSBDemodWorker; + +class ADSBDemod : public BasebandSampleSink, public ChannelAPI { + Q_OBJECT +public: + class MsgConfigureADSBDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ADSBDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureADSBDemod* create(const ADSBDemodSettings& settings, bool force) + { + return new MsgConfigureADSBDemod(settings, force); + } + + private: + ADSBDemodSettings m_settings; + bool m_force; + + MsgConfigureADSBDemod(const ADSBDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + ADSBDemod(DeviceAPI *deviceAPI); + virtual ~ADSBDemod(); + virtual void destroy() { delete this; } + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positive); + virtual void start(); + virtual void stop(); + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = m_channelId; } + virtual const QString& getURI() const { return m_channelIdURI; } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return m_settings.m_inputFrequencyOffset; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const ADSBDemodSettings& settings); + + static void webapiUpdateChannelSettings( + ADSBDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { m_basebandSink->getMagSqLevels(avg, peak, nbSamples); } + void propagateMessageQueueToGUI() { m_basebandSink->setMessageQueueToGUI(getMessageQueueToGUI()); } + + void setTarget(float targetAzimuth, float targetElevation) + { + m_targetAzimuth = targetAzimuth; + m_targetElevation = targetElevation; + m_targetAzElValid = true; + } + void clearTarget() { m_targetAzElValid = false; } + + uint32_t getNumberOfDeviceStreams() const; + + static const QString m_channelIdURI; + static const QString m_channelId; + +private: + DeviceAPI* m_deviceAPI; + QThread *m_thread; + ADSBDemodWorker *m_worker; + ADSBDemodBaseband* m_basebandSink; + ADSBDemodSettings m_settings; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + + bool m_targetAzElValid; + float m_targetAzimuth; + float m_targetElevation; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const ADSBDemodSettings& settings, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const ADSBDemodSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_ADSBDEMOD_H diff --git a/plugins/channelrx/demodadsb/adsbdemodbaseband.cpp b/plugins/channelrx/demodadsb/adsbdemodbaseband.cpp new file mode 100644 index 000000000..d2ed1a268 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodbaseband.cpp @@ -0,0 +1,157 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "adsbdemodbaseband.h" +#include "adsb.h" + +MESSAGE_CLASS_DEFINITION(ADSBDemodBaseband::MsgConfigureADSBDemodBaseband, Message) + +ADSBDemodBaseband::ADSBDemodBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(8000000)); + m_channelizer = new DownChannelizer(&m_sink); + + qDebug("ADSBDemodBaseband::ADSBDemodBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &ADSBDemodBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +ADSBDemodBaseband::~ADSBDemodBaseband() +{ + delete m_channelizer; +} + +void ADSBDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void ADSBDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void ADSBDemodBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0)) + { + SampleVector::iterator part1begin; + SampleVector::iterator part1end; + SampleVector::iterator part2begin; + SampleVector::iterator part2end; + + std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end); + + // first part of FIFO data + if (part1begin != part1end) { + m_channelizer->feed(part1begin, part1end); + } + + // second part of FIFO data (used when block wraps around) + if(part2begin != part2end) { + m_channelizer->feed(part2begin, part2end); + } + + m_sampleFifo.readCommit((unsigned int) count); + } +} + +void ADSBDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool ADSBDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureADSBDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureADSBDemodBaseband& cfg = (MsgConfigureADSBDemodBaseband&) cmd; + qDebug() << "ADSBDemodBaseband::handleMessage: MsgConfigureADSBDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "ADSBDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(8*notif.getSampleRate())); // Need a large FIFO otherwise we get overflows - revist after better upsampling + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else + { + return false; + } +} + +void ADSBDemodBaseband::applySettings(const ADSBDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) + || (settings.m_samplesPerBit != m_settings.m_samplesPerBit) || force) + { + int requestedRate = ADS_B_BITS_PER_SECOND * settings.m_samplesPerBit; + m_channelizer->setChannelization(requestedRate, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int ADSBDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + + +void ADSBDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} diff --git a/plugins/channelrx/demodadsb/adsbdemodbaseband.h b/plugins/channelrx/demodadsb/adsbdemodbaseband.h new file mode 100644 index 000000000..be7367491 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodbaseband.h @@ -0,0 +1,87 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_ADSBDEMODBASEBAND_H +#define INCLUDE_ADSBDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "adsbdemodsink.h" + +class DownChannelizer; + +class ADSBDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureADSBDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ADSBDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureADSBDemodBaseband* create(const ADSBDemodSettings& settings, bool force) + { + return new MsgConfigureADSBDemodBaseband(settings, force); + } + + private: + ADSBDemodSettings m_settings; + bool m_force; + + MsgConfigureADSBDemodBaseband(const ADSBDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + ADSBDemodBaseband(); + ~ADSBDemodBaseband(); + void reset(); + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + int getChannelSampleRate() const; + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { m_sink.getMagSqLevels(avg, peak, nbSamples); } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_sink.setMessageQueueToGUI(messageQueue); } + void setMessageQueueToWorker(MessageQueue *messageQueue) { m_sink.setMessageQueueToWorker(messageQueue); } + void setBasebandSampleRate(int sampleRate); + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + ADSBDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + ADSBDemodSettings m_settings; + QMutex m_mutex; + + bool handleMessage(const Message& cmd); + void applySettings(const ADSBDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_ADSBDEMODBASEBAND_H diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp new file mode 100644 index 000000000..d035cf6d4 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -0,0 +1,1024 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#define _USE_MATH_DEFINES +#include + +#include "device/deviceuiset.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui_adsbdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "gui/crightclickenabler.h" +#include "dsp/dspengine.h" +#include "mainwindow.h" + +#include "adsbdemodreport.h" +#include "adsbdemod.h" +#include "adsbdemodgui.h" +#include "adsb.h" + +// ADS-B table columns +#define ADSB_COL_ICAO 0 +#define ADSB_COL_FLIGHT 1 +#define ADSB_COL_LATITUDE 2 +#define ADSB_COL_LONGITUDE 3 +#define ADSB_COL_ALTITUDE 4 +#define ADSB_COL_SPEED 5 +#define ADSB_COL_HEADING 6 +#define ADSB_COL_VERTICALRATE 7 +#define ADSB_COL_CATEGORY 8 +#define ADSB_COL_STATUS 9 +#define ADSB_COL_RANGE 10 +#define ADSB_COL_AZEL 11 +#define ADSB_COL_TIME 12 +#define ADSB_COL_FRAMECOUNT 13 +#define ADSB_COL_CORRELATION 14 + +const char *Aircraft::m_speedTypeNames[] = { + "GS", "TAS", "IAS" +}; + +ADSBDemodGUI* ADSBDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + ADSBDemodGUI* gui = new ADSBDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void ADSBDemodGUI::destroy() +{ + delete this; +} + +void ADSBDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(); +} + +QByteArray ADSBDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool ADSBDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +// Longitude zone (returns value in range [1,59] +static int cprNL(Real lat) +{ + if (lat == 0.0f) + return 59; + else if ((lat == 87.0f) || (lat == -87.0f)) + return 2; + else if ((lat > 87.0f) || (lat < -87.0f)) + return 1; + else + { + double nz = 15.0; + double n = 1 - std::cos(M_PI / (2.0 * nz)); + double d = std::cos(std::fabs(lat) * M_PI/180.0); + return std::floor((M_PI * 2.0) / std::acos(1.0 - (n/(d*d)))); + } +} + +static int cprN(Real lat, int odd) +{ + int nl = cprNL(lat) - odd; + if (nl > 1) + return nl; + else + return 1; +} + +static Real feetToMetres(Real feet) +{ + return feet * 0.3048; +} + +// Can't use std::fmod, as that works differently for negative numbers +static Real modulus(Real x, Real y) +{ + return x - y * std::floor(x/y); +} + +QVariant AircraftModel::data(const QModelIndex &index, int role) const +{ + int row = index.row(); + if ((row < 0) || (row >= m_aircrafts.count())) + return QVariant(); + if (role == AircraftModel::positionRole) + { + // Coordinates to display the aircraft icon at + QGeoCoordinate coords; + coords.setLatitude(m_aircrafts[row]->m_latitude); + coords.setLongitude(m_aircrafts[row]->m_longitude); + coords.setAltitude(m_aircrafts[row]->m_altitude * 0.3048); // Convert feet to metres + return QVariant::fromValue(coords); + } + else if (role == AircraftModel::headingRole) + { + // What rotation to draw the aircraft icon at + return QVariant::fromValue(m_aircrafts[row]->m_heading); + } + else if (role == AircraftModel::adsbDataRole) + { + // Create the text to go in the bubble next to the aircraft + QStringList list; + if (m_aircrafts[row]->m_flight.length() > 0) + { + list.append(QString("Flight: %1").arg(m_aircrafts[row]->m_flight)); + } + else + { + list.append(QString("ICAO: %1").arg(m_aircrafts[row]->m_icao, 1, 16)); + } + if (m_aircrafts[row]->m_altitudeValid) + { + list.append(QString("Altitude: %1 (ft)").arg(m_aircrafts[row]->m_altitude)); + } + if (m_aircrafts[row]->m_speedValid) + { + list.append(QString("%1: %2 (kn)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(m_aircrafts[row]->m_speed)); + } + if (m_aircrafts[row]->m_verticalRateValid) + { + QString desc; + if (m_aircrafts[row]->m_verticalRate == 0) + desc = "Level flight"; + else if (m_aircrafts[row]->m_verticalRate > 0) + desc = QString("Climbing: %1 (ft/min)").arg(m_aircrafts[row]->m_verticalRate); + else + desc = QString("Descending: %1 (ft/min)").arg(m_aircrafts[row]->m_verticalRate); + list.append(QString(desc)); + } + if ((m_aircrafts[row]->m_status.length() > 0) && m_aircrafts[row]->m_status.compare("No emergency")) + { + list.append(m_aircrafts[row]->m_status); + } + QString data = list.join("\n"); + return QVariant::fromValue(data); + } + else if (role == AircraftModel::aircraftImageRole) + { + // Select an image to use for the aircraft + if (m_aircrafts[row]->m_emitterCategory.length() > 0) + { + if (!m_aircrafts[row]->m_emitterCategory.compare("Heavy")) + return QVariant::fromValue(QString("aircraft_4engine.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("Large")) + return QVariant::fromValue(QString("aircraft_2engine.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("Small")) + return QVariant::fromValue(QString("aircraft_2enginesmall.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("Rotorcraft")) + return QVariant::fromValue(QString("aircraft_helicopter.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("High performance")) + return QVariant::fromValue(QString("aircraft_fighter.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("Light") + || !m_aircrafts[row]->m_emitterCategory.compare("Ultralight") + || !m_aircrafts[row]->m_emitterCategory.compare("Glider/sailplane")) + return QVariant::fromValue(QString("aircraft_light.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("Space vehicle")) + return QVariant::fromValue(QString("aircraft_space.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("UAV")) + return QVariant::fromValue(QString("aircraft_drone.png")); + else if (!m_aircrafts[row]->m_emitterCategory.compare("Emergency vehicle") + || !m_aircrafts[row]->m_emitterCategory.compare("Service vehicle")) + return QVariant::fromValue(QString("map_truck.png")); + else + return QVariant::fromValue(QString("aircraft_2engine.png")); + } + else + return QVariant::fromValue(QString("aircraft_2engine.png")); + } + else if (role == AircraftModel::bubbleColourRole) + { + // Select a background colour for the text bubble next to the aircraft + if (m_aircrafts[row]->m_isBeingTracked) + return QVariant::fromValue(QColor("lightgreen")); + else if ((m_aircrafts[row]->m_status.length() > 0) && m_aircrafts[row]->m_status.compare("No emergency")) + return QVariant::fromValue(QColor("lightred")); + else + return QVariant::fromValue(QColor("lightblue")); + } + return QVariant(); +} + +// Called when we have both lat & long +void ADSBDemodGUI::updatePosition(Aircraft *aircraft) +{ + if (!aircraft->m_positionValid) + { + aircraft->m_positionValid = true; + // Now we have a position, add a plane to the map + QGeoCoordinate coords; + coords.setLatitude(aircraft->m_latitude); + coords.setLongitude(aircraft->m_longitude); + m_aircraftModel.addAircraft(aircraft); + } + // Calculate range, azimuth and elevation to aircraft from station + m_azEl.setTarget(aircraft->m_latitude, aircraft->m_longitude, feetToMetres(aircraft->m_altitude)); + m_azEl.calculate(); + aircraft->m_range = m_azEl.getDistance(); + aircraft->m_azimuth = m_azEl.getAzimuth(); + aircraft->m_elevation = m_azEl.getElevation(); + aircraft->m_rangeItem->setText(QString("%1").arg(aircraft->m_range/1000.0, 0, 'f', 1)); + aircraft->m_azElItem->setText(QString("%1/%2").arg(std::round(aircraft->m_azimuth)).arg(std::round(aircraft->m_elevation))); + if (aircraft == m_trackAircraft) + m_adsbDemod->setTarget(aircraft->m_azimuth, aircraft->m_elevation); +} + +void ADSBDemodGUI::handleADSB(const QByteArray data, const QDateTime dateTime, float correlation) +{ + const char idMap[] = "?ABCDEFGHIJKLMNOPQRSTUVWXYZ????? ???????????????0123456789??????"; + const QString categorySetA[] = { + "None", + "Light", + "Small", + "Large", + "High vortex", + "Heavy", + "High performance", + "Rotorcraft" + }; + const QString categorySetB[] = { + "None", + "Glider/sailplane", + "Lighter-than-air", + "Parachutist", + "Ultralight", + "Reserved", + "UAV", + "Space vehicle" + }; + const QString categorySetC[] = { + "None", + "Emergency vehicle", + "Service vehicle", + "Ground obstruction", + "Cluster obstacle", + "Line obstacle", + "Reserved", + "Reserved" + }; + const QString emergencyStatus[] = { + "No emergency", + "General emergency", + "Lifeguard/Medical", + "Minimum fuel", + "No communications", + "Unlawful interference", + "Downed aircraft", + "Reserved" + }; + + int df = (data[0] >> 3) & ADS_B_DF_MASK; // Downlink format + int ca = data[0] & 0x7; // Capability + unsigned icao = ((data[1] & 0xff) << 16) | ((data[2] & 0xff) << 8) | (data[3] & 0xff); // ICAO aircraft address + int tc = (data[4] >> 3) & 0x1f; // Type code + + Aircraft *aircraft; + if (m_aircraft.contains(icao)) + { + // Update existing aircraft info + aircraft = m_aircraft.value(icao); + } + else + { + // Add new aircraft + aircraft = new Aircraft; + aircraft->m_icao = icao; + m_aircraft.insert(icao, aircraft); + aircraft->m_icaoItem->setText(QString("%1").arg(aircraft->m_icao, 1, 16)); + int row = ui->adsbData->rowCount(); + ui->adsbData->setRowCount(row + 1); + ui->adsbData->setItem(row, ADSB_COL_ICAO, aircraft->m_icaoItem); + ui->adsbData->setItem(row, ADSB_COL_FLIGHT, aircraft->m_flightItem); + ui->adsbData->setItem(row, ADSB_COL_LATITUDE, aircraft->m_latitudeItem); + ui->adsbData->setItem(row, ADSB_COL_LONGITUDE, aircraft->m_longitudeItem); + ui->adsbData->setItem(row, ADSB_COL_ALTITUDE, aircraft->m_altitudeItem); + ui->adsbData->setItem(row, ADSB_COL_SPEED, aircraft->m_speedItem); + ui->adsbData->setItem(row, ADSB_COL_HEADING, aircraft->m_headingItem); + ui->adsbData->setItem(row, ADSB_COL_VERTICALRATE, aircraft->m_verticalRateItem); + ui->adsbData->setItem(row, ADSB_COL_CATEGORY, aircraft->m_emitterCategoryItem); + ui->adsbData->setItem(row, ADSB_COL_STATUS, aircraft->m_statusItem); + ui->adsbData->setItem(row, ADSB_COL_RANGE, aircraft->m_rangeItem); + ui->adsbData->setItem(row, ADSB_COL_AZEL, aircraft->m_azElItem); + ui->adsbData->setItem(row, ADSB_COL_TIME, aircraft->m_timeItem); + ui->adsbData->setItem(row, ADSB_COL_FRAMECOUNT, aircraft->m_adsbFrameCountItem); + ui->adsbData->setItem(row, ADSB_COL_CORRELATION, aircraft->m_correlationItem); + } + aircraft->m_time = dateTime; + QTime time = dateTime.time(); + aircraft->m_timeItem->setText(QString("%1:%2:%3").arg(time.hour(), 2, 10, QLatin1Char('0')).arg(time.minute(), 2, 10, QLatin1Char('0')).arg(time.second(), 2, 10, QLatin1Char('0'))); + aircraft->m_adsbFrameCount++; + aircraft->m_adsbFrameCountItem->setText(QString("%1").arg(aircraft->m_adsbFrameCount)); + + if (correlation < aircraft->m_minCorrelation) + aircraft->m_minCorrelation = correlation; + if (correlation > aircraft->m_maxCorrelation) + aircraft->m_maxCorrelation = correlation; + aircraft->m_sumCorrelation += correlation; + aircraft->m_correlationItem->setText(QString("%1/%2/%3").arg(aircraft->m_minCorrelation, 3, 'f', 1).arg(aircraft->m_sumCorrelation / aircraft->m_adsbFrameCount, 3, 'f', 1).arg(aircraft->m_maxCorrelation, 3, 'f', 1)); + + + if ((tc >= 1) && ((tc <= 4))) + { + // Aircraft identification + int ec = data[4] & 0x7; // Emitter category + if (tc == 4) + aircraft->m_emitterCategory = categorySetA[ec]; + else if (tc == 3) + aircraft->m_emitterCategory = categorySetB[ec]; + else if (tc == 2) + aircraft->m_emitterCategory = categorySetC[ec]; + else + aircraft->m_emitterCategory = QString("Reserved"); + aircraft->m_emitterCategoryItem->setText(aircraft->m_emitterCategory); + + // Flight/callsign - Extract 8 6-bit characters from 6 8-bit bytes, MSB first + unsigned char c[8]; + char callsign[9]; + c[0] = (data[5] >> 2) & 0x3f; // 6 + c[1] = ((data[5] & 0x3) << 4) | ((data[6] & 0xf0) >> 4); // 2+4 + c[2] = ((data[6] & 0xf) << 2) | ((data[7] & 0xc0) >> 6); // 4+2 + c[3] = (data[7] & 0x3f); // 6 + c[4] = (data[8] >> 2) & 0x3f; + c[5] = ((data[8] & 0x3) << 4) | ((data[9] & 0xf0) >> 4); + c[6] = ((data[9] & 0xf) << 2) | ((data[10] & 0xc0) >> 6); + c[7] = (data[10] & 0x3f); + // Map to ASCII + for (int i = 0; i < 8; i++) + callsign[i] = idMap[c[i]]; + callsign[8] = '\0'; + + aircraft->m_flight = QString(callsign); + aircraft->m_flightItem->setText(aircraft->m_flight); + } + else if ((tc >= 5) && (tc <= 8)) + { + // Surface position + } + else if (((tc >= 9) && (tc <= 18)) || ((tc >= 20) && (tc <= 22))) + { + // Airbourne position (9-18 baro, 20-22 GNSS) + int ss = (data[4] >> 1) & 0x3; // Surveillance status + int nicsb = data[4] & 1; // NIC supplement-B + int alt = ((data[5] & 0xff) << 4) | ((data[6] >> 4) & 0xf); // Altitude + int n = ((alt >> 1) & 0x7f0) | (alt & 0xf); + int alt_ft = n * ((alt & 0x10) ? 25 : 100) - 1000; + + aircraft->m_altitude = alt_ft; + aircraft->m_altitudeValid = true; + aircraft->m_altitudeItem->setText(QString("%1").arg(aircraft->m_altitude)); + + int t = (data[6] >> 3) & 1; // Time + int f = (data[6] >> 2) & 1; // CPR odd/even frame + int lat_cpr = ((data[6] & 3) << 15) | ((data[7] & 0xff) << 7) | ((data[8] >> 1) & 0x7f); + int lon_cpr = ((data[8] & 1) << 16) | ((data[9] & 0xff) << 8) | (data[10] & 0xff); + + aircraft->m_cprValid[f] = true; + aircraft->m_cprLat[f] = lat_cpr/131072.0f; + aircraft->m_cprLong[f] = lon_cpr/131072.0f; + + // Check if both odd and even frames are available + // See: https://mode-s.org/decode/adsb/airborne-position.html + // We could compare global + local methods to see if the positions are sensible + if (aircraft->m_cprValid[0] && aircraft->m_cprValid[1]) + { + // Global decode using odd and even frames + + // Calculate latitude + const Real dLatEven = 360.0f/60.0f; + const Real dLatOdd = 360.0f/59.0f; + Real latEven, latOdd; + + int j = std::floor(59.0f*aircraft->m_cprLat[0] - 60.0f*aircraft->m_cprLat[1] + 0.5); + latEven = dLatEven * ((j % 60) + aircraft->m_cprLat[0]); + if (latEven >= 270.0f) + latEven -= 360.0f; + latOdd = dLatOdd * ((j % 59) + aircraft->m_cprLat[1]); + if (latOdd >= 270.0f) + latOdd -= 360.0f; + if (!f) + aircraft->m_latitude = latEven; + else + aircraft->m_latitude = latOdd; + aircraft->m_latitudeItem->setText(QString("%1").arg(aircraft->m_latitude)); + + // Check if both frames in same latitude zone + int latEvenNL = cprNL(latEven); + int latOddNL = cprNL(latOdd); + if (latEvenNL == latOddNL) + { + // Calculate longitude + if (!f) + { + int ni = cprN(latEven, 0); + int m = std::floor(aircraft->m_cprLong[0] * (latEvenNL - 1) - aircraft->m_cprLong[1] * latEvenNL + 0.5f); + aircraft->m_longitude = (360.0f/ni) * ((m % ni) + aircraft->m_cprLong[0]); + } + else + { + int ni = cprN(latOdd, 1); + int m = std::floor(aircraft->m_cprLong[0] * (latOddNL - 1) - aircraft->m_cprLong[1] * latOddNL + 0.5f); + aircraft->m_longitude = (360.0f/ni) * ((m % ni) + aircraft->m_cprLong[1]); + } + if (aircraft->m_longitude > 180.0f) + aircraft->m_longitude -= 360.0f; + aircraft->m_longitudeItem->setText(QString("%1").arg(aircraft->m_longitude)); + updatePosition(aircraft); + } + } + else + { + // Local decode using a single aircraft position + location of receiver + // Only valid if within 180nm + + // Caclulate latitude + const Real dLatEven = 360.0f/60.0f; + const Real dLatOdd = 360.0f/59.0f; + Real dLat = f ? dLatOdd : dLatEven; + int j = std::floor(m_azEl.getLocationSpherical().m_latitude/dLat) + std::floor(modulus(m_azEl.getLocationSpherical().m_latitude, dLat)/dLat - aircraft->m_cprLat[f] + 0.5); + aircraft->m_latitude = dLat * (j + aircraft->m_cprLat[f]); + // Add suffix of L to indicate local decode + aircraft->m_latitudeItem->setText(QString("%1 L").arg(aircraft->m_latitude)); + + // Caclulate longitude + Real dLong; + int latNL = cprNL(aircraft->m_latitude); + if (f == 0) + { + if (latNL > 0) + dLong = 360.0 / latNL; + else + dLong = 360.0; + } + else + { + if ((latNL - 1) > 0) + dLong = 360.0 / (latNL - 1); + else + dLong = 360.0; + } + int m = std::floor(m_azEl.getLocationSpherical().m_longitude/dLong) + std::floor(modulus(m_azEl.getLocationSpherical().m_longitude, dLong)/dLong - aircraft->m_cprLong[f] + 0.5); + aircraft->m_longitude = dLong * (m + aircraft->m_cprLong[f]); + // Add suffix of L to indicate local decode + aircraft->m_longitudeItem->setText(QString("%1 L").arg(aircraft->m_longitude)); + updatePosition(aircraft); + } + } + else if (tc == 19) + { + // Airbourne velocity + int st = data[4] & 0x7; // Subtype + int ic = (data[5] >> 7) & 1; // Intent change flag + int nac = (data[5] >> 3) & 0x3; // Velocity uncertainty + if ((st == 1) || (st == 2)) + { + // Ground speed + int s_ew = (data[5] >> 2) & 1; // East-west velocity sign + int v_ew = ((data[5] & 0x3) << 8) | (data[6] & 0xff); // East-west velocity + int s_ns = (data[7] >> 7) & 1; // North-south velocity sign + int v_ns = ((data[7] & 0x7f) << 3) | ((data[8] >> 5) & 0x7); // North-south velocity + + int v_we; + int v_sn; + int v; + int h; + + if (s_ew) + v_we = -1 * (v_ew - 1); + else + v_we = v_ew - 1; + if (s_ns) + v_sn = -1 * (v_ns - 1); + else + v_sn = v_ns - 1; + v = (int)std::sqrt(v_we*v_we + v_sn*v_sn); + h = std::atan2(v_we, v_sn) * 360.0/(2.0*M_PI); + if (h < 0) + h += 360; + + aircraft->m_heading = h; + aircraft->m_headingValid = true; + aircraft->m_speed = v; + aircraft->m_speedType = Aircraft::GS; + aircraft->m_speedValid = true; + aircraft->m_headingItem->setText(QString("%1").arg(aircraft->m_heading)); + aircraft->m_speedItem->setText(QString("%1").arg(aircraft->m_speed)); + } + else + { + // Airspeed + int s_hdg = (data[5] >> 2) & 1; // Heading status + int hdg = ((data[5] & 0x3) << 8) | (data[6] & 0xff); // Heading + if (s_hdg) + { + aircraft->m_heading = hdg/1024.0f*360.0f; + aircraft->m_headingValid = true; + aircraft->m_headingItem->setText(QString("%1").arg(aircraft->m_heading)); + } + + int as_t = (data[7] >> 7) & 1; // Airspeed type + int as = ((data[7] & 0x7f) << 3) | ((data[8] >> 5) & 0x7); // Airspeed + + aircraft->m_speed = as; + aircraft->m_speedType = as_t ? Aircraft::IAS : Aircraft::TAS; + aircraft->m_speedValid = true; + aircraft->m_speedItem->setText(QString("%1").arg(aircraft->m_speed)); + } + int vrsrc = (data[8] >> 4) & 1; // Vertical rate source + int s_vr = (data[8] >> 3) & 1; // Vertical rate sign + int vr = ((data[8] & 0x7) << 6) | ((data[9] >> 2) & 0x3f); // Vertical rate + aircraft->m_verticalRate = (vr-1)*64*(s_vr?-1:1); + aircraft->m_verticalRateValid = true; + aircraft->m_verticalRateItem->setText(QString("%1").arg(aircraft->m_verticalRate)); + + int s_dif = (data[10] >> 7) & 1; // Diff from baro alt, sign + int dif = data[10] & 0x7f; // Diff from baro alt + } + else if (tc == 28) + { + // Aircraft status + int st = data[4] & 0x7; // Subtype + int es = (data[5] >> 5) & 0x7; // Emergeny state + if (st == 1) + aircraft->m_status = emergencyStatus[es]; + else + aircraft->m_status = QString(""); + aircraft->m_statusItem->setText(aircraft->m_status); + } + else if (tc == 29) + { + // Target state and status + } + else if (tc == 31) + { + // Aircraft operation status + } + + // Update aircraft in map + if (aircraft->m_positionValid) + { + m_aircraftModel.aircraftUpdated(aircraft); + } + +} + +bool ADSBDemodGUI::handleMessage(const Message& message) +{ + if (ADSBDemodReport::MsgReportADSB::match(message)) + { + ADSBDemodReport::MsgReportADSB& report = (ADSBDemodReport::MsgReportADSB&) message; + handleADSB(report.getData(), report.getDateTime(), report.getPreambleCorrelation()); + return true; + } + else if (ADSBDemod::MsgConfigureADSBDemod::match(message)) + { + qDebug("ADSBDemodGUI::handleMessage: ADSBDemod::MsgConfigureADSBDemod"); + const ADSBDemod::MsgConfigureADSBDemod& cfg = (ADSBDemod::MsgConfigureADSBDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + + return false; +} + +void ADSBDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void ADSBDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void ADSBDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void ADSBDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void ADSBDemodGUI::on_rfBW_valueChanged(int value) +{ + Real bw = (Real)value; + ui->rfBWText->setText(QString("%1M").arg(bw / 1000000.0, 0, 'f', 1)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void ADSBDemodGUI::on_threshold_valueChanged(int value) +{ + Real threshold = ((Real)value)/10.0f; + ui->thresholdText->setText(QString("%1").arg(threshold, 0, 'f', 1)); + m_settings.m_correlationThreshold = threshold; + applySettings(); +} + +void ADSBDemodGUI::on_beastEnabled_stateChanged(int state) +{ + m_settings.m_beastEnabled = state == Qt::Checked; + // Don't disable host/port - so they can be entered before connecting + applySettings(); +} + +void ADSBDemodGUI::on_host_editingFinished(QString value) +{ + m_settings.m_beastHost = value; + applySettings(); +} + +void ADSBDemodGUI::on_port_valueChanged(int value) +{ + m_settings.m_beastPort = value; + applySettings(); +} + +void ADSBDemodGUI::on_adsbData_cellDoubleClicked(int row, int column) +{ + // Get ICAO of aircraft in row double clicked + int icao = ui->adsbData->item(row, 0)->text().toInt(nullptr, 16); + if (column == ADSB_COL_ICAO) + { + // Search for aircraft on planespotters.net + QString icaoUpper = QString("%1").arg(icao, 1, 16).toUpper(); + QDesktopServices::openUrl(QUrl(QString("https://www.planespotters.net/hex/%1").arg(icaoUpper))); + } + else if (m_aircraft.contains(icao)) + { + Aircraft *aircraft = m_aircraft.value(icao); + + if (column == ADSB_COL_FLIGHT) + { + if (aircraft->m_flight.length() > 0) + { + // Search for flight on flightradar24 + QDesktopServices::openUrl(QUrl(QString("https://www.flightradar24.com/%1").arg(aircraft->m_flight.trimmed()))); + } + } + else + { + if (column == ADSB_COL_AZEL) + { + if (m_trackAircraft) + { + // Restore colour of current target + m_trackAircraft->m_isBeingTracked = false; + m_aircraftModel.aircraftUpdated(m_trackAircraft); + } + // Track this aircraft + m_trackAircraft = aircraft; + if (aircraft->m_positionValid) + m_adsbDemod->setTarget(aircraft->m_azimuth, aircraft->m_elevation); + // Change colour of new target + aircraft->m_isBeingTracked = true; + m_aircraftModel.aircraftUpdated(aircraft); + } + // Center map view on aircraft if it has a valid position + if (aircraft->m_positionValid) + { + QQuickItem *item = ui->map->rootObject(); + QObject *object = item->findChild("map"); + if(object != NULL) + { + QGeoCoordinate geocoord = object->property("center").value(); + geocoord.setLatitude(aircraft->m_latitude); + geocoord.setLongitude(aircraft->m_longitude); + object->setProperty("center", QVariant::fromValue(geocoord)); + } + } + } + } +} + +void ADSBDemodGUI::on_spb_currentIndexChanged(int value) +{ + m_settings.m_samplesPerBit = (value + 2) * 2; + applySettings(); +} + +void ADSBDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void ADSBDemodGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_adsbDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + + +ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::ADSBDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_basicSettingsShown(false), + m_doApplySettings(true), + m_tickCount(0), + m_trackAircraft(nullptr) +{ + ui->setupUi(this); + ui->map->rootContext()->setContextProperty("aircraftModel", &m_aircraftModel); + + setAttribute(Qt::WA_DeleteOnClose, true); + + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_adsbDemod = reinterpret_cast(rxChannel); //new ADSBDemod(m_deviceUISet->m_deviceSourceAPI); + m_adsbDemod->setMessageQueueToGUI(getInputMessageQueue()); + m_adsbDemod->propagateMessageQueueToGUI(); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::red); + m_channelMarker.setBandwidth(5000); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setTitle("ADS-B Demodulator"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + m_settings.setChannelMarker(&m_channelMarker); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + ui->adsbData->resizeColumnsToContents(); + + // Get station position + Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); + Real stationLongitude = MainCore::instance()->getSettings().getLongitude(); + Real stationAltitude = MainCore::instance()->getSettings().getAltitude(); + m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); + + // Centre map at My Position + QQuickItem *item = ui->map->rootObject(); + QObject *object = item->findChild("map"); + if(object != NULL) + { + QGeoCoordinate coords = object->property("center").value(); + coords.setLatitude(stationLatitude); + coords.setLongitude(stationLongitude); + object->setProperty("center", QVariant::fromValue(coords)); + } + // Move antenna icon to My Position + QObject *stationObject = item->findChild("station"); + if(stationObject != NULL) + { + QGeoCoordinate coords = stationObject->property("coordinate").value(); + coords.setLatitude(stationLatitude); + coords.setLongitude(stationLongitude); + coords.setAltitude(stationAltitude); + stationObject->setProperty("coordinate", QVariant::fromValue(coords)); + stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName())); + } + + displaySettings(); + applySettings(true); +} + +ADSBDemodGUI::~ADSBDemodGUI() +{ + delete ui; + QHash::iterator i = m_aircraft.begin(); + while (i != m_aircraft.end()) + { + Aircraft *a = i.value(); + delete a; + ++i; + } +} + +void ADSBDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + qDebug() << "ADSBDemodGUI::applySettings"; + + ADSBDemod::MsgConfigureADSBDemod* message = ADSBDemod::MsgConfigureADSBDemod::create( m_settings, force); + m_adsbDemod->getInputMessageQueue()->push(message); + } +} + +void ADSBDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + ui->rfBWText->setText(QString("%1M").arg(m_settings.m_rfBandwidth / 1000000.0, 0, 'f', 1)); + ui->rfBW->setValue((int)m_settings.m_rfBandwidth); + + ui->spb->setCurrentIndex(m_settings.m_samplesPerBit/2-2); + + ui->thresholdText->setText(QString("%1").arg(m_settings.m_correlationThreshold, 0, 'f', 1)); + ui->threshold->setValue((int)(m_settings.m_correlationThreshold*10)); + + ui->beastEnabled->setChecked(m_settings.m_beastEnabled); + ui->host->setText(m_settings.m_beastHost); + ui->port->setValue(m_settings.m_beastPort); + + displayStreamIndex(); + + blockApplySettings(false); +} + +void ADSBDemodGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void ADSBDemodGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void ADSBDemodGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void ADSBDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void ADSBDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_adsbDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbPeak = CalcDb::dbPower(magsqPeak); + + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(tr("%1 dB").arg(powDbAvg, 0, 'f', 1)); + } + + m_tickCount++; + + // Tick is called 20x a second - lets check this every 10 seconds + if (m_tickCount % (20*10) == 0) + { + // Remove aircraft that haven't been heard of for a minute as probably out of range + QDateTime now = QDateTime::currentDateTime(); + qint64 nowSecs = now.toSecsSinceEpoch(); + QHash::iterator i = m_aircraft.begin(); + while (i != m_aircraft.end()) + { + Aircraft *aircraft = i.value(); + qint64 secondsSinceLastFrame = nowSecs - aircraft->m_time.toSecsSinceEpoch(); + if (secondsSinceLastFrame >= m_settings.m_removeTimeout) + { + // Don't try to track it anymore + if (m_trackAircraft == aircraft) + { + m_adsbDemod->clearTarget(); + m_trackAircraft = nullptr; + } + // Remove map model + m_aircraftModel.removeAircraft(aircraft); + // Remove row from table + ui->adsbData->removeRow(aircraft->m_icaoItem->row()); + // Remove aircraft from hash + i = m_aircraft.erase(i); + // And finally free its memory + delete aircraft; + } + else + ++i; + } + } +} diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.h b/plugins/channelrx/demodadsb/adsbdemodgui.h new file mode 100644 index 000000000..68c8e2511 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodgui.h @@ -0,0 +1,272 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_ADSBDEMODGUI_H +#define INCLUDE_ADSBDEMODGUI_H + +#include +#include +#include +#include + +#include "channel/channelgui.h" +#include "dsp/dsptypes.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/messagequeue.h" +#include "util/azel.h" + +#include "adsbdemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ADSBDemod; + +namespace Ui { + class ADSBDemodGUI; +} + +// Data about an aircraft extracted from an ADS-B frames +struct Aircraft { + int m_icao; // 24-bit ICAO aircraft address + QString m_flight; // Flight callsign + Real m_latitude; // Latitude in decimal degrees + Real m_longitude; // Longitude in decimal degrees + int m_altitude; // Altitude in feet + int m_speed; // Speed in knots + enum SpeedType { + GS, // Ground speed + TAS, // True air speed + IAS // Indicated air speed + } m_speedType; + static const char *m_speedTypeNames[]; + int m_heading; // Heading in degrees + int m_verticalRate; // Vertical climb rate in ft/min + QString m_emitterCategory; // Aircraft type + QString m_status; // Aircraft status + Real m_range; // Distance from station to aircraft + Real m_azimuth; // Azimuth from station to aircraft + Real m_elevation; // Elevation from station to aicraft; + QDateTime m_time; // When last updated + + bool m_positionValid; // Indicates if we have valid data for the above fields + bool m_altitudeValid; + bool m_speedValid; + bool m_headingValid; + bool m_verticalRateValid; + + // State for calculating position using two CPR frames + bool m_cprValid[2]; + Real m_cprLat[2]; + Real m_cprLong[2]; + + int m_adsbFrameCount; // Number of ADS-B frames for this aircraft + float m_minCorrelation; + float m_maxCorrelation; + float m_sumCorrelation; + bool m_isBeingTracked; // Are we tracking this aircraft + + // GUI table items for above data + QTableWidgetItem *m_icaoItem; + QTableWidgetItem *m_flightItem; + QTableWidgetItem *m_latitudeItem; + QTableWidgetItem *m_longitudeItem; + QTableWidgetItem *m_altitudeItem; + QTableWidgetItem *m_speedItem; + QTableWidgetItem *m_headingItem; + QTableWidgetItem *m_verticalRateItem; + QTableWidgetItem *m_emitterCategoryItem; + QTableWidgetItem *m_statusItem; + QTableWidgetItem *m_rangeItem; + QTableWidgetItem *m_azElItem; + QTableWidgetItem *m_timeItem; + QTableWidgetItem *m_adsbFrameCountItem; + QTableWidgetItem *m_correlationItem; + + Aircraft() : + m_icao(0), + m_latitude(0), + m_longitude(0), + m_altitude(0), + m_speed(0), + m_heading(0), + m_verticalRate(0), + m_azimuth(0), + m_elevation(0), + m_positionValid(false), + m_altitudeValid(false), + m_speedValid(false), + m_headingValid(false), + m_verticalRateValid(false), + m_adsbFrameCount(0), + m_minCorrelation(INFINITY), + m_maxCorrelation(-INFINITY), + m_sumCorrelation(0.0f), + m_isBeingTracked(false) + { + for (int i = 0; i < 2; i++) + { + m_cprValid[i] = false; + } + // These are deleted by QTableWidget + m_icaoItem = new QTableWidgetItem(); + m_flightItem = new QTableWidgetItem(); + m_latitudeItem = new QTableWidgetItem(); + m_longitudeItem = new QTableWidgetItem(); + m_altitudeItem = new QTableWidgetItem(); + m_speedItem = new QTableWidgetItem(); + m_headingItem = new QTableWidgetItem(); + m_verticalRateItem = new QTableWidgetItem(); + m_emitterCategoryItem = new QTableWidgetItem(); + m_statusItem = new QTableWidgetItem(); + m_rangeItem = new QTableWidgetItem(); + m_azElItem = new QTableWidgetItem(); + m_timeItem = new QTableWidgetItem(); + m_adsbFrameCountItem = new QTableWidgetItem(); + m_correlationItem = new QTableWidgetItem(); + } +}; + +// Aircraft data model used by QML map item +class AircraftModel : public QAbstractListModel { + Q_OBJECT + +public: + using QAbstractListModel::QAbstractListModel; + enum MarkerRoles{ + positionRole = Qt::UserRole + 1, + headingRole = Qt::UserRole + 2, + adsbDataRole = Qt::UserRole + 3, + aircraftImageRole = Qt::UserRole + 4, + bubbleColourRole = Qt::UserRole + 5 + }; + + Q_INVOKABLE void addAircraft(Aircraft *aircraft) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_aircrafts.append(aircraft); + endInsertRows(); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override { + Q_UNUSED(parent) + return m_aircrafts.count(); + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void aircraftUpdated(Aircraft *aircraft) { + int row = m_aircrafts.indexOf(aircraft); + if (row >= 0) + { + QModelIndex idx = index(row); + emit dataChanged(idx, idx); + } + } + + void removeAircraft(Aircraft *aircraft) { + int row = m_aircrafts.indexOf(aircraft); + if (row >= 0) + { + beginRemoveRows(QModelIndex(), row, row); + m_aircrafts.removeAt(row); + endRemoveRows(); + } + } + + QHash roleNames() const { + QHash roles; + roles[positionRole] = "position"; + roles[headingRole] = "heading"; + roles[adsbDataRole] = "adsbData"; + roles[aircraftImageRole] = "aircraftImage"; + roles[bubbleColourRole] = "bubbleColour"; + return roles; + } + +private: + QList m_aircrafts; +}; + +class ADSBDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static ADSBDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + Ui::ADSBDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + ADSBDemodSettings m_settings; + bool m_basicSettingsShown; + bool m_doApplySettings; + + ADSBDemod* m_adsbDemod; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QHash m_aircraft; // Hashed on ICAO + AircraftModel m_aircraftModel; + + AzEl m_azEl; // Position of station + Aircraft *m_trackAircraft; // Aircraft we want to track in Channel Report + + explicit ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~ADSBDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + bool handleMessage(const Message& message); + void updatePosition(Aircraft *aircraft); + void handleADSB(const QByteArray data, const QDateTime dateTime, float correlation); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int value); + void on_threshold_valueChanged(int value); + void on_adsbData_cellDoubleClicked(int row, int column); + void on_spb_currentIndexChanged(int value); + void on_beastEnabled_stateChanged(int state); + void on_host_editingFinished(QString value); + void on_port_valueChanged(int value); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); +signals: + void homePositionChanged(); +}; + +#endif // INCLUDE_ADSBDEMODGUI_H diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.ui b/plugins/channelrx/demodadsb/adsbdemodgui.ui new file mode 100644 index 000000000..a0ffdff16 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodgui.ui @@ -0,0 +1,667 @@ + + + ADSBDemodGUI + + + + 0 + 0 + 350 + 1019 + + + + + 0 + 0 + + + + + 350 + 0 + + + + + Liberation Sans + 9 + + + + ADS-B Demodulator + + + + + 0 + 0 + 340 + 101 + + + + + 300 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 60 + 0 + + + + Channel power + + + Qt::RightToLeft + + + -100.0 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + + + RFBW + + + + + + + RF bandwidth + + + 1000000 + + + 3000000 + + + 100000 + + + 1000000 + + + 2300000 + + + Qt::Horizontal + + + + + + + 2.3M + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + + + + SR + + + + + + + + 40 + 0 + + + + Demodulator sample rate. This should ideally be matched to baseband sample rate + + + + 4 + + + + + 6 + + + + + 8 + + + + + 10 + + + + + 12 + + + + + + + + M + + + + + + + Qt::Vertical + + + + + + + Threshold + + + + + + + + 24 + 24 + + + + Correlation threshold. Lower values will increase the number of frames that can be received, but require more processing. + + + -100 + + + 20 + + + 0 + + + + + + + + 30 + 0 + + + + -15.0 + + + + + + + + + + + Enable feeding of received ADS-B messages in Beast binary format to the specifed server + + + Feed + + + + + + + Server + + + + + + + Hostname of the server to feed + + + feed.adsbexchange.com + + + + + + + Port + + + + + + + + 60 + 0 + + + + Port the server is listening on + + + 1024 + + + 65535 + + + 30005 + + + + + + + + + + + 0 + 114 + 341 + 291 + + + + + 0 + 0 + + + + ADS-B Data + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + Segoe UI + 9 + + + + QAbstractItemView::NoEditTriggers + + + + ICAO ID + + + + + + + + Flight No. + + + + + Latitude (°) + + + + + + + + Longitude (°) + + + + + + + + Altitude (ft) + + + + + Speed (kn) + + + + + Heading (°) + + + + + Climb (ft/min) + + + + + Category + + + + + Status + + + + + Range (km) + + + + + Az/El (°) + + + + + Updated + + + + + RX Frames + + + + + Correlation + + + + + + + + + + 10 + 420 + 331 + 581 + + + + + 0 + 0 + + + + Map + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 0 + + + + + 100 + 400 + + + + Aircraft location map + + + QQuickWidget::SizeRootObjectToView + + + + qrc:/map.qml + + + + + + + + + + QQuickWidget + QWidget +
QtQuickWidgets/QQuickWidget
+
+ + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+
+ + deltaFrequency + rfBW + spb + threshold + beastEnabled + host + port + adsbData + map + + + + + +
diff --git a/plugins/channelrx/demodadsb/adsbdemodreport.cpp b/plugins/channelrx/demodadsb/adsbdemodreport.cpp new file mode 100644 index 000000000..14c2a6d17 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodreport.cpp @@ -0,0 +1,21 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 "adsbdemodreport.h" + +MESSAGE_CLASS_DEFINITION(ADSBDemodReport::MsgReportADSB, Message) diff --git a/plugins/channelrx/demodadsb/adsbdemodreport.h b/plugins/channelrx/demodadsb/adsbdemodreport.h new file mode 100644 index 000000000..3ee354896 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodreport.h @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 PLUGINS_CHANNELRX_DEMOADSB_ADSBDEMODREPORT_H_ +#define PLUGINS_CHANNELRX_DEMOADSB_ADSBDEMODREPORT_H_ + +#include +#include +#include + +#include "dsp/dsptypes.h" +#include "util/message.h" + +class ADSBDemodReport : public QObject +{ + Q_OBJECT +public: + class MsgReportADSB : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QByteArray getData() const { return m_data; } + QDateTime getDateTime() const { return m_dateTime; } + float getPreambleCorrelation() const { return m_premableCorrelation; } + + static MsgReportADSB* create(QByteArray data, float premableCorrelation) + { + return new MsgReportADSB(data, premableCorrelation); + } + + private: + QByteArray m_data; + QDateTime m_dateTime; + float m_premableCorrelation; + + MsgReportADSB(QByteArray data, float premableCorrelation) : + Message(), + m_data(data), + m_premableCorrelation(premableCorrelation) + { + m_dateTime = QDateTime::currentDateTime(); + } + }; + +public: + ADSBDemodReport() {} + ~ADSBDemodReport() {} +}; + +#endif // PLUGINS_CHANNELRX_DEMOADSB_ADSBDEMODREPORT_H_ diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp new file mode 100644 index 000000000..f24621a48 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp @@ -0,0 +1,142 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "adsbdemodsettings.h" + +ADSBDemodSettings::ADSBDemodSettings() : + m_channelMarker(0) +{ + resetToDefaults(); +} + +void ADSBDemodSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 2*1300000; + m_correlationThreshold = 0.0f; + m_samplesPerBit = 6; + m_removeTimeout = 60; + m_beastEnabled = false; + m_beastHost = "feed.adsbexchange.com"; + m_beastPort = 30005; + m_rgbColor = QColor(255, 0, 0).rgb(); + m_title = "ADS-B Demodulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; +} + +QByteArray ADSBDemodSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeReal(2, m_rfBandwidth); + s.writeReal(3, m_correlationThreshold); + s.writeS32(4, m_samplesPerBit); + s.writeS32(5, m_removeTimeout); + s.writeBool(6, m_beastEnabled); + s.writeString(7, m_beastHost); + s.writeU32(8, m_beastPort); + + s.writeU32(9, m_rgbColor); + if (m_channelMarker) { + s.writeBlob(10, m_channelMarker->serialize()); + } + s.writeString(11, m_title); + s.writeBool(12, m_useReverseAPI); + s.writeString(13, m_reverseAPIAddress); + s.writeU32(14, m_reverseAPIPort); + s.writeU32(15, m_reverseAPIDeviceIndex); + s.writeU32(16, m_reverseAPIChannelIndex); + s.writeS32(17, m_streamIndex); + + return s.final(); +} + +bool ADSBDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + qint32 tmp; + uint32_t utmp; + + if (m_channelMarker) + { + d.readBlob(10, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readS32(1, &tmp, 0); + m_inputFrequencyOffset = tmp; + d.readReal(2, &m_rfBandwidth, 2*1300000); + d.readReal(3, &m_correlationThreshold, 1.0f); + d.readS32(4, &m_samplesPerBit, 6); + d.readS32(5, &m_removeTimeout, 60); + d.readBool(6, &m_beastEnabled, false); + d.readString(7, &m_beastHost, "feed.adsbexchange.com"); + d.readU32(8, &utmp, 0); + if ((utmp > 1023) && (utmp < 65535)) { + m_beastPort = utmp; + } else { + m_beastPort = 30005; + } + + d.readU32(9, &m_rgbColor, QColor(255, 0, 0).rgb()); + d.readString(11, &m_title, "ADS-B Demodulator"); + d.readBool(12, &m_useReverseAPI, false); + d.readString(13, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(14, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(15, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(16, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readS32(17, &m_streamIndex, 0); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.h b/plugins/channelrx/demodadsb/adsbdemodsettings.h new file mode 100644 index 000000000..9b02d1b51 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -0,0 +1,58 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 PLUGINS_CHANNELRX_DEMODADSB_ADSBDEMODSETTINGS_H_ +#define PLUGINS_CHANNELRX_DEMODADSB_ADSBDEMODSETTINGS_H_ + +#include +#include +#include +#include "dsp/dsptypes.h" + +class Serializable; + +struct ADSBDemodSettings +{ + int32_t m_inputFrequencyOffset; + Real m_rfBandwidth; + Real m_correlationThreshold; + int m_samplesPerBit; + int m_removeTimeout; // Time in seconds before removing an aircraft, unless a new frame is received + bool m_beastEnabled; + QString m_beastHost; + uint16_t m_beastPort; + + quint32 m_rgbColor; + QString m_title; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + Serializable *m_channelMarker; + + ADSBDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* PLUGINS_CHANNELRX_DEMODADSB_ADSBDEMODSETTINGS_H_ */ diff --git a/plugins/channelrx/demodadsb/adsbdemodsink.cpp b/plugins/channelrx/demodadsb/adsbdemodsink.cpp new file mode 100644 index 000000000..8812e4082 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodsink.cpp @@ -0,0 +1,278 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include +#include + +#include "util/stepfunctions.h" +#include "util/db.h" +#include "util/crc.h" +#include "audio/audiooutput.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/devicesamplemimo.h" +#include "device/deviceapi.h" + +#include "adsbdemodreport.h" +#include "adsbdemodsink.h" +#include "adsb.h" + +ADSBDemodSink::ADSBDemodSink() : + m_channelSampleRate(6000000), + m_channelFrequencyOffset(0), + m_sampleIdx(0), + m_sampleCount(0), + m_skipCount(0), + m_magsq(0.0f), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToGUI(nullptr), + m_sampleBuffer(nullptr), + m_preamble(nullptr) +{ + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +ADSBDemodSink::~ADSBDemodSink() +{ + delete m_sampleBuffer; + delete m_preamble; +} + +void ADSBDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + + for (SampleVector::const_iterator it = begin; it != end; ++it) + { + Complex c(it->real(), it->imag()); + c *= m_nco.nextIQ(); + + if (m_interpolatorDistance == 1.0f) + { + processOneSample(c); + } + else if (m_interpolatorDistance < 1.0f) // interpolate + { + while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + else // decimate + { + if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + } + +} + +void ADSBDemodSink::processOneSample(Complex &ci) +{ + Real sample; + + double magsqRaw = ci.real()*ci.real() + ci.imag()*ci.imag(); + Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsqSum += magsq; + + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + m_magsqCount++; + + sample = std::sqrtf(magsq); + m_sampleBuffer[m_sampleCount] = sample; + m_sampleCount++; + + // Do we have enough data for a frame + if ((m_sampleCount >= m_totalSamples) && (m_skipCount == 0)) + { + int startIdx = m_sampleCount - m_totalSamples; + + // Correlate received signal with expected preamble + Real preambleCorrelation = 0.0f; + for (int i = 0; i < ADS_B_PREAMBLE_CHIPS*m_samplesPerChip; i++) + preambleCorrelation += m_preamble[i] * m_sampleBuffer[startIdx+i]; + + // If the correlation is exactly 0, it's probably no signal + if ((preambleCorrelation > m_settings.m_correlationThreshold) && (preambleCorrelation != 0.0f)) + { + // Skip over preamble + startIdx += m_settings.m_samplesPerBit*ADS_B_PREAMBLE_BITS; + + // Demodulate waveform to bytes + unsigned char data[ADS_B_ES_BYTES]; + int byteIdx = 0; + int currentBit; + unsigned char currentByte = 0; + bool adsbOnly = true; + int df; + + for (int bit = 0; bit < ADS_B_ES_BITS; bit++) + { + // PPM (Pulse position modulation) - Each bit spreads to two chips, 1->10, 0->01 + // Determine if bit is 1 or 0, by seeing which chip has largest combined energy over the sampling period + Real oneSum = 0.0f; + Real zeroSum = 0.0f; + for (int i = 0; i < m_samplesPerChip; i++) + { + oneSum += m_sampleBuffer[startIdx+i]; + zeroSum += m_sampleBuffer[startIdx+m_samplesPerChip+i]; + } + currentBit = oneSum > zeroSum; + startIdx += m_settings.m_samplesPerBit; + // Convert bit to bytes - MSB first + currentByte |= currentBit << (7-(bit & 0x7)); + if ((bit & 0x7) == 0x7) + { + data[byteIdx++] = currentByte; + currentByte = 0; + // Don't try to demodulate any further, if this isn't an ADS-B frame + // to help reduce processing overhead + if (adsbOnly && (bit == 7)) + { + df = ((data[0] >> 3) & ADS_B_DF_MASK); + if ((df != 17) && (df != 18)) + break; + } + } + } + + // Is ADS-B? + df = ((data[0] >> 3) & ADS_B_DF_MASK); + if ((df == 17) || (df == 18)) + { + crcadsb crc; + //int icao = (data[1] << 16) | (data[2] << 8) | data[3]; // ICAO aircraft address + int parity = (data[11] << 16) | (data[12] << 8) | data[13]; // Parity / CRC + + crc.calculate(data, ADS_B_ES_BYTES-3); + if (parity == crc.get()) + { + // Got a valid frame + // Don't try to re-demodulate the same frame + m_skipCount = (ADS_B_ES_BITS+ADS_B_PREAMBLE_BITS)*ADS_B_CHIPS_PER_BIT*m_samplesPerChip; + // Pass to GUI + if (getMessageQueueToGUI()) + { + ADSBDemodReport::MsgReportADSB *msg = ADSBDemodReport::MsgReportADSB::create(QByteArray((char*)data, sizeof(data)), preambleCorrelation); + getMessageQueueToGUI()->push(msg); + } + // Pass to worker + if (getMessageQueueToWorker()) + { + ADSBDemodReport::MsgReportADSB *msg = ADSBDemodReport::MsgReportADSB::create(QByteArray((char*)data, sizeof(data)), preambleCorrelation); + getMessageQueueToWorker()->push(msg); + } + } + } + } + } + if (m_skipCount > 0) + m_skipCount--; + if (m_sampleCount >= 2*m_totalSamples) + { + // Copy second half of buffer to first + memcpy(&m_sampleBuffer[0], &m_sampleBuffer[m_totalSamples], m_totalSamples*sizeof(Real)); + m_sampleCount = m_totalSamples; + } + m_sampleIdx++; + +} + +void ADSBDemodSink::init(int samplesPerBit) +{ + if (m_sampleBuffer) + delete m_sampleBuffer; + if (m_preamble) + delete m_preamble; + + m_totalSamples = samplesPerBit*(ADS_B_PREAMBLE_BITS+ADS_B_ES_BITS); + m_samplesPerChip = samplesPerBit/ADS_B_CHIPS_PER_BIT; + + m_sampleBuffer = new Real[2*m_totalSamples]; + m_preamble = new Real[ADS_B_PREAMBLE_CHIPS*m_samplesPerChip]; + // Possibly don't look for first chip + const int preambleChips[] = {1, -1, 1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, -1}; + for (int i = 0; i < ADS_B_PREAMBLE_CHIPS; i++) + { + for (int j = 0; j < m_samplesPerChip; j++) + { + m_preamble[i*m_samplesPerChip+j] = preambleChips[i]; + } + } +} + +void ADSBDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "ADSBDemodSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) || + (channelSampleRate != m_channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((channelSampleRate != m_channelSampleRate) || force) + { + m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2); + m_interpolatorDistanceRemain = 0; + m_interpolatorDistance = (Real) channelSampleRate / (Real) (ADS_B_BITS_PER_SECOND * m_settings.m_samplesPerBit); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void ADSBDemodSink::applySettings(const ADSBDemodSettings& settings, bool force) +{ + qDebug() << "ADSBDemodSink::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_correlationThreshold: " << settings.m_correlationThreshold + << " m_samplesPerBit: " << settings.m_samplesPerBit + << " force: " << force; + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) + || (settings.m_samplesPerBit != m_settings.m_samplesPerBit) || force) + { + m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); + m_interpolatorDistanceRemain = 0; + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) (ADS_B_BITS_PER_SECOND * settings.m_samplesPerBit); + } + if ((settings.m_samplesPerBit != m_settings.m_samplesPerBit) || force) + { + init(settings.m_samplesPerBit); + } + + m_settings = settings; +} diff --git a/plugins/channelrx/demodadsb/adsbdemodsink.h b/plugins/channelrx/demodadsb/adsbdemodsink.h new file mode 100644 index 000000000..b57970c7b --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodsink.h @@ -0,0 +1,106 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_ADSBDEMODSINK_H +#define INCLUDE_ADSBDEMODSINK_H + +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "util/movingaverage.h" + +#include "adsbdemodsettings.h" + +class ADSBDemodSink : public ChannelSampleSink { +public: + ADSBDemodSink(); + ~ADSBDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + + void init(int samplesPerBit); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const ADSBDemodSettings& settings, bool force = false); + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_messageQueueToGUI = messageQueue; } + void setMessageQueueToWorker(MessageQueue *messageQueue) { m_messageQueueToWorker = messageQueue; } + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + int m_channelSampleRate; + int m_channelFrequencyOffset; + ADSBDemodSettings m_settings; + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + int m_sampleIdx; + int m_sampleCount; + int m_skipCount; // Samples to skip, because we've already received a frame + Real *m_sampleBuffer; + Real *m_preamble; + + int m_totalSamples; // These two values are derived from samplesPerBit + int m_samplesPerChip; + + double m_magsq; //!< displayed averaged value + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + + MovingAverageUtil m_movingAverage; + + MessageQueue *m_messageQueueToGUI; + MessageQueue *m_messageQueueToWorker; + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToGUI() { return m_messageQueueToGUI; } + MessageQueue *getMessageQueueToWorker() { return m_messageQueueToWorker; } +}; + +#endif // INCLUDE_ADSBDEMODSINK_H diff --git a/plugins/channelrx/demodadsb/adsbdemodwebapiadapter.cpp b/plugins/channelrx/demodadsb/adsbdemodwebapiadapter.cpp new file mode 100644 index 000000000..d18add79d --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodwebapiadapter.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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 "SWGChannelSettings.h" +#include "adsbdemod.h" +#include "adsbdemodwebapiadapter.h" + +ADSBDemodWebAPIAdapter::ADSBDemodWebAPIAdapter() +{} + +ADSBDemodWebAPIAdapter::~ADSBDemodWebAPIAdapter() +{} + +int ADSBDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAdsbDemodSettings(new SWGSDRangel::SWGADSBDemodSettings()); + response.getAdsbDemodSettings()->init(); + ADSBDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int ADSBDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + ADSBDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodadsb/adsbdemodwebapiadapter.h b/plugins/channelrx/demodadsb/adsbdemodwebapiadapter.h new file mode 100644 index 000000000..7e480d031 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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_ADSBDEMOD_WEBAPIADAPTER_H +#define INCLUDE_ADSBDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "adsbdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class ADSBDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + ADSBDemodWebAPIAdapter(); + virtual ~ADSBDemodWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + ADSBDemodSettings m_settings; +}; + +#endif // INCLUDE_ADSBDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodadsb/adsbdemodworker.cpp b/plugins/channelrx/demodadsb/adsbdemodworker.cpp new file mode 100644 index 000000000..c254f4bbc --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodworker.cpp @@ -0,0 +1,197 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "adsbdemodworker.h" +#include "adsbdemodreport.h" + +MESSAGE_CLASS_DEFINITION(ADSBDemodWorker::MsgConfigureADSBDemodWorker, Message) + +ADSBDemodWorker::ADSBDemodWorker() : + m_running(false), + m_mutex(QMutex::Recursive) +{ + connect(&m_heartbeatTimer, SIGNAL(timeout()), this, SLOT(heartbeat())); + connect(&m_socket, SIGNAL(readyRead()),this, SLOT(recv())); +} + +ADSBDemodWorker::~ADSBDemodWorker() +{ + m_inputMessageQueue.clear(); +} + +void ADSBDemodWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +bool ADSBDemodWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_heartbeatTimer.start(60*1000); + m_running = true; + return m_running; +} + +void ADSBDemodWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + m_heartbeatTimer.stop(); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = false; +} + +void ADSBDemodWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool ADSBDemodWorker::handleMessage(const Message& message) +{ + if (MsgConfigureADSBDemodWorker::match(message)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureADSBDemodWorker& cfg = (MsgConfigureADSBDemodWorker&) message; + + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else if (ADSBDemodReport::MsgReportADSB::match(message)) + { + ADSBDemodReport::MsgReportADSB& report = (ADSBDemodReport::MsgReportADSB&) message; + handleADSB(report.getData(), report.getDateTime(), report.getPreambleCorrelation()); + return true; + } + else + { + return false; + } +} + +void ADSBDemodWorker::applySettings(const ADSBDemodSettings& settings, bool force) +{ + qDebug() << "ADSBDemodWorker::applySettings:" + << " m_beastEnabled: " << settings.m_beastEnabled + << " m_beastHost: " << settings.m_beastHost + << " m_beastPort: " << settings.m_beastPort + << " force: " << force; + + if ((settings.m_beastEnabled != m_settings.m_beastEnabled) + || (settings.m_beastHost != m_settings.m_beastHost) + || (settings.m_beastPort != m_settings.m_beastPort) || force) + { + // Close any existing connection + if (m_socket.isOpen()) + m_socket.close(); + // Open connection + if (settings.m_beastEnabled) + m_socket.connectToHost(settings.m_beastHost, settings.m_beastPort); + } + + m_settings = settings; +} + +void ADSBDemodWorker::recv() +{ + // Not expecting to receving anything from server + qDebug() << "ADSBDemodWorker::recv"; + qDebug() << m_socket.readAll(); +} + +void ADSBDemodWorker::send(const char *data, int length) +{ + if (m_settings.m_beastEnabled) + { + // Reopen connection if it was lost + if (!m_socket.isOpen()) + m_socket.connectToHost(m_settings.m_beastHost, m_settings.m_beastPort); + // Send data + m_socket.write(data, length); + } +} + +#define BEAST_ESC 0x1a + +char *ADSBDemodWorker::escape(char *p, char c) +{ + *p++ = c; + if (c == BEAST_ESC) + *p++ = BEAST_ESC; + return p; +} + +// Forward ADS-B data in Beast binary format to specified server +// See: https://wiki.jetvision.de/wiki/Mode-S_Beast:Data_Output_Formats +void ADSBDemodWorker::handleADSB(QByteArray data, const QDateTime dateTime, float correlation) +{ + char beastBinary[2+6*2+1*2+14*2]; + int length; + char *p = beastBinary; + qint64 timestamp; + unsigned char signalStrength; + + timestamp = dateTime.toMSecsSinceEpoch(); + + if (correlation > 255) + signalStrength = 255; + if (correlation < 1) + signalStrength = 1; + else + signalStrength = (unsigned char)correlation; + + *p++ = BEAST_ESC; + *p++ = '3'; // Mode-S long + + p = escape(p, timestamp >> 56); // Big-endian timestamp + p = escape(p, timestamp >> 48); + p = escape(p, timestamp >> 32); + p = escape(p, timestamp >> 24); + p = escape(p, timestamp >> 16); + p = escape(p, timestamp >> 8); + p = escape(p, timestamp); + + p = escape(p, signalStrength); // Signal strength + + for (int i = 0; i < data.length(); i++) // ADS-B data + p = escape(p, data[i]); + + length = p - beastBinary; + + send(beastBinary, length); +} + +// Periodically send heartbeat to keep connection alive +void ADSBDemodWorker::heartbeat() +{ + const char heartbeat[] = {BEAST_ESC, '1', 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Mode AC packet + send(heartbeat, sizeof(heartbeat)); +} diff --git a/plugins/channelrx/demodadsb/adsbdemodworker.h b/plugins/channelrx/demodadsb/adsbdemodworker.h new file mode 100644 index 000000000..8a592205b --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbdemodworker.h @@ -0,0 +1,87 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_ADSBDEMODWORKER_H +#define INCLUDE_ADSBDEMODWORKER_H + +#include +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" + +#include "adsbdemodsettings.h" + +class ADSBDemodWorker : public QObject +{ + Q_OBJECT +public: + class MsgConfigureADSBDemodWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ADSBDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureADSBDemodWorker* create(const ADSBDemodSettings& settings, bool force) + { + return new MsgConfigureADSBDemodWorker(settings, force); + } + + private: + ADSBDemodSettings m_settings; + bool m_force; + + MsgConfigureADSBDemodWorker(const ADSBDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + ADSBDemodWorker(); + ~ADSBDemodWorker(); + void reset(); + bool startWork(); + void stopWork(); + bool isRunning() const { return m_running; } + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + + MessageQueue m_inputMessageQueue; + ADSBDemodSettings m_settings; + bool m_running; + QMutex m_mutex; + QTimer m_heartbeatTimer; + QTcpSocket m_socket; + + bool handleMessage(const Message& cmd); + void applySettings(const ADSBDemodSettings& settings, bool force = false); + void send(const char *data, int length); + char *escape(char *p, char c); + void handleADSB(QByteArray data, const QDateTime dateTime, float correlation); + +private slots: + void handleInputMessages(); + void recv(); + void heartbeat(); +}; + +#endif // INCLUDE_ADSBDEMODWORKER_H diff --git a/plugins/channelrx/demodadsb/adsbplugin.cpp b/plugins/channelrx/demodadsb/adsbplugin.cpp new file mode 100644 index 000000000..2b44dd522 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#include "adsbplugin.h" +#ifndef SERVER_MODE +#include "adsbdemodgui.h" +#endif +#include "adsbdemod.h" +#include "adsbdemodwebapiadapter.h" +#include "adsbplugin.h" + +const PluginDescriptor ADSBPlugin::m_pluginDescriptor = { + ADSBDemod::m_channelId, + QString("ADS-B Demodulator"), + QString("4.20.0"), + QString("(c) Jon Beniston, M7RCE"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +ADSBPlugin::ADSBPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& ADSBPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void ADSBPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register demodulator + m_pluginAPI->registerRxChannel(ADSBDemod::m_channelIdURI, ADSBDemod::m_channelId, this); +} + +void ADSBPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + ADSBDemod *instance = new ADSBDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* ADSBPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + return 0; +} +#else +ChannelGUI* ADSBPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return ADSBDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* ADSBPlugin::createChannelWebAPIAdapter() const +{ + return new ADSBDemodWebAPIAdapter(); +} diff --git a/plugins/channelrx/demodadsb/adsbplugin.h b/plugins/channelrx/demodadsb/adsbplugin.h new file mode 100644 index 000000000..b43a49051 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_ADSBPLUGIN_H +#define INCLUDE_ADSBPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class ADSBPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.adsbdemod") + +public: + explicit ADSBPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_ADSBPLUGIN_H diff --git a/plugins/channelrx/demodadsb/readme.md b/plugins/channelrx/demodadsb/readme.md new file mode 100644 index 000000000..dd2f603c5 --- /dev/null +++ b/plugins/channelrx/demodadsb/readme.md @@ -0,0 +1,86 @@ +

ADS-B demodulator plugin

+ +

Introduction

+ +This plugin can be used to receive and display ADS-B aircraft information. This is information about an aircraft, such as position, altitude, heading and speed, broadcast by aircraft on 1090MHz, in the 1090ES (Extended Squitter) format. 1090ES frames have a chip rate of 2Mchip/s, so the baseband sample rate should be set to be greater than 2MSa/s. + +

Interface

+ +![ADS-B Demodulator plugin GUI](../../../doc/img/ADSBdemod_plugin.png) + +

1: Frequency shift from center frequency of reception value

+ +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. Left click on a digit sets the cursor position at this digit. + +

2: Channel power

+ +Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. + +

3: Level meter in dB

+ + - top bar (green): average value + - bottom bar (blue green): instantaneous peak value + - tip vertical bar (bright green): peak hold value + +

4: RF bandwidth

+ +This is the bandwidth in MHz of the channel signal before demodulation. + +

5: SR - Channel Sample Rate

+ +This specifies the channel sample rate the demodulator uses. Values of 4M-12MSa/s are supported, 2MSa/s steps. Ideally, this should be set to the same value as the baseband sample rate (the sample rate received from the radio). If it is different from the baseband sample rate, interpolation or decimation will be performed as needed to match the rates. However, interpolation currently requires a significant amount of processing power and may result in dropped samples. + +Higher channel sample rates may help decode more frames, but will require more processing power. + +

6: Threshold

+ +This sets the correlation threshold between the received signal and expected 1090ES preamble, that is required to be exceeded before the demodulator will try to decode a frame. Lower values should decode more frames, but will require more processing power. + +

7: Feed

+ +Checking Feed enables feeding received ADS-B frames to aggregators such as ADS-B Exchange: https://www.adsbexchange.com The server name and port to send the frames to should be entered in the Server and Port fields. For ADS-B Exchange, set Server to feed.adsbexchange.com and Port to 30005. You can check if you are feeding data to ADS-B Exchange (after about 30 seconds) at: https://www.adsbexchange.com/myip/ Frames are forwarded in the Beast binary format as described here: https://wiki.jetvision.de/wiki/Mode-S_Beast:Data_Output_Formats + +

ADS-B Data

+ +The table displays the decoded ADS-B data for each aircraft. The data is not all able to be transmitted in a single ADS-B frame, so the table displays an amalgamation of the latest received data of each type. + +![ADS-B Demodulator Data](../../../doc/img/ADSBdemod_plugin_table.png) + +* ICAO ID - 24-bit hexidecimal ICAO aircraft address. This is unique for each aircraft. +* Flight No. - Airline flight number or callsign. +* Latitude - Vertical position coordinate, in decimal degrees. +* Longitude - Horizontal position coordinate, in decimal degrees. +* Altitude - Altitude in feet. +* Speed - Speed (either ground speed, indicated airspeed, or true airspeed) in knots. +* Heading - The direction the aircraft is heading, in degrees. +* Climb - The vertical climb rate (or descent rate if negative) in feet/minute. +* Categoty - The vehicle category, such as Light, Large, Heavy or Rotorcraft. +* Range - The range (distance) to the aircraft from the receiving antenna in kilometres. The location of the receiving antenna is set under the Preferences > My Position menu. +* Az/El - The azimuth and elevation angles to the aircraft from the receiving antenna in degrees. These values can be sent to a rotator controller to track the aircraft. +* Updated - The local time at which the last ADS-B message was received. +* RX Frames - A count of the number of ADS-B frames received from this aircraft. +* Correlation - Displays the minimum, average and maximum of the preamable correlation for each recevied frame. These values can be used to help select a threshold setting. + +A suffix of L in the latitude and longitude columns, indicates the position is based on a local decode, using a single received frame and the position of the radio station. No suffix will be added for a global decode, which is based upon receving and odd and even frame. +If an ADS-B frame has not been received from an aircraft for 60 seconds, the aircraft is removed from the table and map. + +Double clicking in an ICAO ID cell will open a Web browser and search for the corresponding aircraft on https://www.planespotters.net/ +Double clicking in an Flight No cell will open a Web browser and search for the corresponding flight on https://www.flightradar24.com/ +Double clicking in an Az/El cell will set the aircraft as the active target. The azimuth and elevation to the aicraft will be sent to a rotator controller plugin. The aircraft text will be coloured green, rather than blue, on the map. +Double clicking on any other cell in the table will centre the map on the corresponding aircraft. + +

Map

+ +The map displays aircraft locations and data geographically. + +![ADS-B Demodulator Map](../../../doc/img/ADSBdemod_plugin_map.png) + +The initial antenna location is placed according to My Position set under the Preferences > My Position menu. The position is only updated when the ADS-B demodulator plugin is first opened. + +Aircraft are only placed upon the map when a position can be calculated, which can require several frames to be received. + +To pan around the map, click the left mouse button and drag. To zoom in or out, use the mouse scroll wheel. + +

Attribution

+ +Icons are by Alice Design, Alex Ahineev, Verry Obito, Sean Maldjia, Tinashe Mugayi, Andreas Vögele and Angriawan Ditya Zulkarnain from the Noun Project https://thenounproject.com/ diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 58a75ed42..6d80a3cd6 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -1,4 +1,7 @@ project(feature) +if (Qt5SerialPort_FOUND) + add_subdirectory(gs232controller) +endif() add_subdirectory(rigctlserver) add_subdirectory(simpleptt) diff --git a/plugins/feature/gs232controller/CMakeLists.txt b/plugins/feature/gs232controller/CMakeLists.txt new file mode 100644 index 000000000..c5d99df76 --- /dev/null +++ b/plugins/feature/gs232controller/CMakeLists.txt @@ -0,0 +1,64 @@ +project(gs232controller) + +set(gs232controller_SOURCES + gs232controller.cpp + gs232controllersettings.cpp + gs232controllerplugin.cpp + gs232controllerworker.cpp + gs232controllerwebapiadapter.cpp +) + +set(gs232controller_HEADERS + gs232controller.h + gs232controllersettings.h + gs232controllerplugin.h + gs232controllerreport.h + gs232controllerworker.h + gs232controllerwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(gs232controller_SOURCES + ${gs232controller_SOURCES} + gs232controllergui.cpp + gs232controllergui.ui + ) + set(gs232controller_HEADERS + ${gs232controller_HEADERS} + gs232controllergui.h + ) + + set(TARGET_NAME featuregs232controller) + set(TARGET_LIB Qt5::Widgets) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featuregs232controllersrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${gs232controller_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + Qt5::SerialPort + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + # Run deployqt for serial libraries + include(DeployQt) + windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/../../../sdrgui/resources) +endif() diff --git a/plugins/feature/gs232controller/gs232controller.cpp b/plugins/feature/gs232controller/gs232controller.cpp new file mode 100644 index 000000000..d88b3dc90 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controller.cpp @@ -0,0 +1,425 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "gs232controllerworker.h" +#include "gs232controller.h" + +MESSAGE_CLASS_DEFINITION(GS232Controller::MsgConfigureGS232Controller, Message) +MESSAGE_CLASS_DEFINITION(GS232Controller::MsgStartStop, Message) + +const QString GS232Controller::m_featureIdURI = "sdrangel.feature.gs232controller"; +const QString GS232Controller::m_featureId = "GS232Controller"; + +GS232Controller::GS232Controller(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_ptt(false) +{ + qDebug("GS232Controller::GS232Controller: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_worker = new GS232ControllerWorker(webAPIAdapterInterface); + m_state = StIdle; + m_errorMessage = "GS232Controller error"; +} + +GS232Controller::~GS232Controller() +{ + if (m_worker->isRunning()) { + stop(); + } + + delete m_worker; +} + +void GS232Controller::start() +{ + qDebug("GS232Controller::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(); + + GS232ControllerWorker::MsgConfigureGS232ControllerWorker *msg = GS232ControllerWorker::MsgConfigureGS232ControllerWorker::create(m_settings, true); + m_worker->getInputMessageQueue()->push(msg); +} + +void GS232Controller::stop() +{ + qDebug("GS232Controller::stop"); + m_worker->stopWork(); + m_state = StIdle; + m_thread.quit(); + m_thread.wait(); +} + +bool GS232Controller::handleMessage(const Message& cmd) +{ + if (MsgConfigureGS232Controller::match(cmd)) + { + MsgConfigureGS232Controller& cfg = (MsgConfigureGS232Controller&) cmd; + qDebug() << "GS232Controller::handleMessage: MsgConfigureGS232Controller"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + qDebug() << "GS232Controller::handleMessage: MsgStartStop: start:" << cfg.getStartStop(); + + if (cfg.getStartStop()) { + start(); + } else { + stop(); + } + + return true; + } + else if (GS232ControllerSettings::MsgChannelIndexChange::match(cmd)) + { + GS232ControllerSettings::MsgChannelIndexChange& cfg = (GS232ControllerSettings::MsgChannelIndexChange&) cmd; + int newChannelIndex = cfg.getIndex(); + qDebug() << "GS232Controller::handleMessage: MsgChannelIndexChange: " << newChannelIndex; + GS232ControllerSettings settings = m_settings; + settings.m_channelIndex = newChannelIndex; + applySettings(settings, false); + + if (getMessageQueueToGUI()) + { + GS232ControllerSettings::MsgChannelIndexChange *msg = new GS232ControllerSettings::MsgChannelIndexChange(cfg); + getMessageQueueToGUI()->push(msg); + } + + return true; + } + else + { + return false; + } +} + +QByteArray GS232Controller::serialize() const +{ + return m_settings.serialize(); +} + +bool GS232Controller::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureGS232Controller *msg = MsgConfigureGS232Controller::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureGS232Controller *msg = MsgConfigureGS232Controller::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void GS232Controller::applySettings(const GS232ControllerSettings& settings, bool force) +{ + qDebug() << "GS232Controller::applySettings:" + << " m_azimuth: " << settings.m_azimuth + << " m_elevation: " << settings.m_elevation + << " m_serialPort: " << settings.m_serialPort + << " m_baudRate: " << settings.m_baudRate + << " m_track: " << settings.m_track + << " m_deviceIndex: " << settings.m_deviceIndex + << " m_channelIndex: " << settings.m_channelIndex + << " 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_azimuth != settings.m_azimuth) || force) { + reverseAPIKeys.append("azimuth"); + } + if ((m_settings.m_elevation != settings.m_elevation) || force) { + reverseAPIKeys.append("elevation"); + } + if ((m_settings.m_serialPort != settings.m_serialPort) || force) { + reverseAPIKeys.append("serialPort"); + } + if ((m_settings.m_baudRate != settings.m_baudRate) || force) { + reverseAPIKeys.append("baudRate"); + } + if ((m_settings.m_track != settings.m_track) || force) { + reverseAPIKeys.append("track"); + } + if ((m_settings.m_deviceIndex != settings.m_deviceIndex) || force) { + reverseAPIKeys.append("deviceIndex"); + } + if ((m_settings.m_channelIndex != settings.m_channelIndex) || force) { + reverseAPIKeys.append("channelIndex"); + } + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + GS232ControllerWorker::MsgConfigureGS232ControllerWorker *msg = GS232ControllerWorker::MsgConfigureGS232ControllerWorker::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 GS232Controller::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + getFeatureStateStr(*response.getState()); + MsgStartStop *msg = MsgStartStop::create(run); + getInputMessageQueue()->push(msg); + return 202; +} + +int GS232Controller::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setGs232ControllerSettings(new SWGSDRangel::SWGGS232ControllerSettings()); + response.getGs232ControllerSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int GS232Controller::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + GS232ControllerSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureGS232Controller *msg = MsgConfigureGS232Controller::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("GS232Controller::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureGS232Controller *msgToGUI = MsgConfigureGS232Controller::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void GS232Controller::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const GS232ControllerSettings& settings) +{ + response.getGs232ControllerSettings()->setAzimuth(settings.m_azimuth); + response.getGs232ControllerSettings()->setElevation(settings.m_elevation); + response.getGs232ControllerSettings()->setSerialPort(new QString(settings.m_serialPort)); + response.getGs232ControllerSettings()->setBaudRate(settings.m_baudRate); + response.getGs232ControllerSettings()->setTrack(settings.m_track); + response.getGs232ControllerSettings()->setDeviceIndex(settings.m_deviceIndex); + response.getGs232ControllerSettings()->setChannelIndex(settings.m_channelIndex); + + if (response.getGs232ControllerSettings()->getTitle()) { + *response.getGs232ControllerSettings()->getTitle() = settings.m_title; + } else { + response.getGs232ControllerSettings()->setTitle(new QString(settings.m_title)); + } + + response.getGs232ControllerSettings()->setRgbColor(settings.m_rgbColor); + response.getGs232ControllerSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getGs232ControllerSettings()->getReverseApiAddress()) { + *response.getGs232ControllerSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getGs232ControllerSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getGs232ControllerSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getGs232ControllerSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex); + response.getGs232ControllerSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex); +} + +void GS232Controller::webapiUpdateFeatureSettings( + GS232ControllerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("azimuth")) { + settings.m_azimuth = response.getGs232ControllerSettings()->getAzimuth(); + } + if (featureSettingsKeys.contains("elevation")) { + settings.m_elevation = response.getGs232ControllerSettings()->getElevation(); + } + if (featureSettingsKeys.contains("serialPort")) { + settings.m_serialPort = *response.getGs232ControllerSettings()->getSerialPort(); + } + if (featureSettingsKeys.contains("baudRate")) { + settings.m_serialPort = response.getGs232ControllerSettings()->getBaudRate(); + } + if (featureSettingsKeys.contains("track")) { + settings.m_track = response.getGs232ControllerSettings()->getTrack() != 0; + } + if (featureSettingsKeys.contains("deviceIndex")) { + settings.m_deviceIndex = response.getGs232ControllerSettings()->getDeviceIndex(); + } + if (featureSettingsKeys.contains("channelIndex")) { + settings.m_channelIndex = response.getGs232ControllerSettings()->getChannelIndex(); + } + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getGs232ControllerSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getGs232ControllerSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getGs232ControllerSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getGs232ControllerSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getGs232ControllerSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getGs232ControllerSettings()->getReverseApiDeviceIndex(); + } + if (featureSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIFeatureIndex = response.getGs232ControllerSettings()->getReverseApiChannelIndex(); + } +} + +void GS232Controller::webapiReverseSendSettings(QList& featureSettingsKeys, const GS232ControllerSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("GS232Controller")); + swgFeatureSettings->setGs232ControllerSettings(new SWGSDRangel::SWGGS232ControllerSettings()); + SWGSDRangel::SWGGS232ControllerSettings *swgGS232ControllerSettings = swgFeatureSettings->getGs232ControllerSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("azimuth") || force) { + swgGS232ControllerSettings->setAzimuth(settings.m_azimuth); + } + if (featureSettingsKeys.contains("elevation") || force) { + swgGS232ControllerSettings->setElevation(settings.m_elevation); + } + if (featureSettingsKeys.contains("serialPort") || force) { + swgGS232ControllerSettings->setSerialPort(new QString(settings.m_serialPort)); + } + if (featureSettingsKeys.contains("baudRate") || force) { + swgGS232ControllerSettings->setBaudRate(settings.m_baudRate); + } + if (featureSettingsKeys.contains("track") || force) { + swgGS232ControllerSettings->setTrack(settings.m_track); + } + if (featureSettingsKeys.contains("deviceIndex") || force) { + swgGS232ControllerSettings->setDeviceIndex(settings.m_deviceIndex); + } + if (featureSettingsKeys.contains("channelIndex") || force) { + swgGS232ControllerSettings->setChannelIndex(settings.m_channelIndex); + } + if (featureSettingsKeys.contains("title") || force) { + swgGS232ControllerSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgGS232ControllerSettings->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 GS232Controller::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "GS232Controller::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("GS232Controller::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/gs232controller/gs232controller.h b/plugins/feature/gs232controller/gs232controller.h new file mode 100644 index 000000000..d101992f3 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controller.h @@ -0,0 +1,140 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GS232CONTROLLER_H_ +#define INCLUDE_FEATURE_GS232CONTROLLER_H_ + +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "gs232controllersettings.h" + +class WebAPIAdapterInterface; +class GS232ControllerWorker; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class GS232Controller : public Feature +{ + Q_OBJECT +public: + class MsgConfigureGS232Controller : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const GS232ControllerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureGS232Controller* create(const GS232ControllerSettings& settings, bool force) { + return new MsgConfigureGS232Controller(settings, force); + } + + private: + GS232ControllerSettings m_settings; + bool m_force; + + MsgConfigureGS232Controller(const GS232ControllerSettings& 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) + { } + }; + + GS232Controller(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~GS232Controller(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual const QString& getURI() const { return m_featureIdURI; } + virtual void getIdentifier(QString& id) const { id = m_featureId; } + 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 GS232ControllerSettings& settings); + + static void webapiUpdateFeatureSettings( + GS232ControllerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const QString m_featureIdURI; + static const QString m_featureId; + +private: + QThread m_thread; + GS232ControllerWorker *m_worker; + GS232ControllerSettings m_settings; + bool m_ptt; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const GS232ControllerSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const GS232ControllerSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_GS232CONTROLLER_H_ diff --git a/plugins/feature/gs232controller/gs232controllergui.cpp b/plugins/feature/gs232controller/gs232controllergui.cpp new file mode 100644 index 000000000..f39650630 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllergui.cpp @@ -0,0 +1,447 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "feature/featureuiset.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "mainwindow.h" +#include "device/deviceuiset.h" + +#include "ui_gs232controllergui.h" +#include "gs232controller.h" +#include "gs232controllergui.h" +#include "gs232controllerreport.h" + +GS232ControllerGUI* GS232ControllerGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + GS232ControllerGUI* gui = new GS232ControllerGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void GS232ControllerGUI::destroy() +{ + delete this; +} + +void GS232ControllerGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray GS232ControllerGUI::serialize() const +{ + qDebug("GS232ControllerGUI::serialize: %d", m_settings.m_channelIndex); + return m_settings.serialize(); +} + +bool GS232ControllerGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + qDebug("GS232ControllerGUI::deserialize: %d", m_settings.m_channelIndex); + updateDeviceSetList(); + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool GS232ControllerGUI::handleMessage(const Message& message) +{ + if (GS232Controller::MsgConfigureGS232Controller::match(message)) + { + qDebug("GS232ControllerGUI::handleMessage: GS232Controller::MsgConfigureGS232Controller"); + const GS232Controller::MsgConfigureGS232Controller& cfg = (GS232Controller::MsgConfigureGS232Controller&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (GS232ControllerSettings::MsgChannelIndexChange::match(message)) + { + const GS232ControllerSettings::MsgChannelIndexChange& cfg = (GS232ControllerSettings::MsgChannelIndexChange&) message; + int newChannelIndex = cfg.getIndex(); + qDebug("GS232ControllerGUI::handleMessage: GS232ControllerSettings::MsgChannelIndexChange: %d", newChannelIndex); + ui->channel->blockSignals(true); + ui->channel->setCurrentIndex(newChannelIndex); + m_settings.m_channelIndex = newChannelIndex; + ui->channel->blockSignals(false); + + return true; + } else if (GS232ControllerReport::MsgReportAzAl::match(message)) + { + GS232ControllerReport::MsgReportAzAl& azAl = (GS232ControllerReport::MsgReportAzAl&) message; + if (azAl.getType() == GS232ControllerReport::AzAlType::TARGET) + { + ui->azimuth->setValue(std::round(azAl.getAzimuth())); + ui->elevation->setValue(std::round(azAl.getElevation())); + } + else + { + ui->azimuthCurrentText->setText(QString("%1").arg(std::round(azAl.getAzimuth()))); + ui->elevationCurrentText->setText(QString("%1").arg(std::round(azAl.getElevation()))); + } + return true; + } + + return false; +} + +void GS232ControllerGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void GS232ControllerGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +GS232ControllerGUI::GS232ControllerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::GS232ControllerGUI), + 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_gs232Controller = reinterpret_cast(feature); + m_gs232Controller->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); + + updateSerialPortList(); + updateDeviceSetList(); + displaySettings(); + applySettings(true); +} + +GS232ControllerGUI::~GS232ControllerGUI() +{ + delete ui; +} + +void GS232ControllerGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void GS232ControllerGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + blockApplySettings(true); + ui->azimuth->setValue(m_settings.m_azimuth); + ui->elevation->setValue(m_settings.m_elevation); + if (m_settings.m_serialPort.length() > 0) + ui->serialPort->lineEdit()->setText(m_settings.m_serialPort); + ui->baudRate->setCurrentText(QString("%1").arg(m_settings.m_baudRate)); + ui->track->setChecked(m_settings.m_track); + blockApplySettings(false); +} + +void GS232ControllerGUI::updateSerialPortList() +{ + ui->serialPort->clear(); + QList serialPorts = QSerialPortInfo::availablePorts(); + QListIterator i(serialPorts); + while (i.hasNext()) + { + QSerialPortInfo info = i.next(); + ui->serialPort->addItem(info.portName()); + } +} + +void GS232ControllerGUI::updateDeviceSetList() +{ + MainWindow *mainWindow = MainWindow::getInstance(); + std::vector& deviceUISets = mainWindow->getDeviceUISets(); + std::vector::const_iterator it = deviceUISets.begin(); + + ui->device->blockSignals(true); + + ui->device->clear(); + unsigned int deviceIndex = 0; + + for (; it != deviceUISets.end(); ++it, deviceIndex++) + { + DSPDeviceSourceEngine *deviceSourceEngine = (*it)->m_deviceSourceEngine; + DSPDeviceSinkEngine *deviceSinkEngine = (*it)->m_deviceSinkEngine; + DSPDeviceMIMOEngine *deviceMIMOEngine = (*it)->m_deviceMIMOEngine; + + if (deviceSourceEngine) { + ui->device->addItem(QString("R%1").arg(deviceIndex), deviceIndex); + } + } + + int newDeviceIndex; + + if (it != deviceUISets.begin()) + { + if (m_settings.m_deviceIndex < 0) { + ui->device->setCurrentIndex(0); + } else { + ui->device->setCurrentIndex(m_settings.m_deviceIndex); + } + + newDeviceIndex = ui->device->currentData().toInt(); + } + else + { + newDeviceIndex = -1; + } + + + if (newDeviceIndex != m_settings.m_deviceIndex) + { + qDebug("GS232ControllerGUI::updateDeviceSetLists: device index changed: %d", newDeviceIndex); + m_settings.m_deviceIndex = newDeviceIndex; + } + + updateChannelList(); + + ui->device->blockSignals(false); +} + +bool GS232ControllerGUI::updateChannelList() +{ + int newChannelIndex; + ui->channel->blockSignals(true); + ui->channel->clear(); + + if (m_settings.m_deviceIndex < 0) + { + newChannelIndex = -1; + } + else + { + MainWindow *mainWindow = MainWindow::getInstance(); + std::vector& deviceUISets = mainWindow->getDeviceUISets(); + DeviceUISet *deviceUISet = deviceUISets[m_settings.m_deviceIndex]; + int nbChannels = deviceUISet->getNumberOfChannels(); + + for (int ch = 0; ch < nbChannels; ch++) { + ui->channel->addItem(QString("%1").arg(ch), ch); + } + + if (nbChannels > 0) + { + if (m_settings.m_channelIndex < 0) { + ui->channel->setCurrentIndex(0); + } else { + ui->channel->setCurrentIndex(m_settings.m_channelIndex); + } + + newChannelIndex = ui->channel->currentIndex(); + } + else + { + newChannelIndex = -1; + } + } + + ui->channel->blockSignals(false); + + if (newChannelIndex != m_settings.m_channelIndex) + { + qDebug("GS232ControllerGUI::updateChannelList: channel index changed: %d", newChannelIndex); + m_settings.m_channelIndex = newChannelIndex; + return true; + } + + return false; +} + +void GS232ControllerGUI::leaveEvent(QEvent*) +{ +} + +void GS232ControllerGUI::enterEvent(QEvent*) +{ +} + +void GS232ControllerGUI::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 GS232ControllerGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + GS232Controller::MsgStartStop *message = GS232Controller::MsgStartStop::create(checked); + m_gs232Controller->getInputMessageQueue()->push(message); + } +} + +void GS232ControllerGUI::on_devicesRefresh_clicked() +{ + updateDeviceSetList(); + displaySettings(); + applySettings(); +} + +void GS232ControllerGUI::on_device_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_deviceIndex = ui->device->currentData().toInt(); + updateChannelList(); + applySettings(); + } +} + +void GS232ControllerGUI::on_channel_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_channelIndex = index; + applySettings(); + } +} + +void GS232ControllerGUI::on_serialPort_currentIndexChanged(int index) +{ + m_settings.m_serialPort = ui->serialPort->currentText(); + applySettings(); +} + +void GS232ControllerGUI::on_baudRate_currentIndexChanged(int index) +{ + m_settings.m_baudRate = ui->baudRate->currentText().toInt(); + applySettings(); +} + +void GS232ControllerGUI::on_azimuth_valueChanged(int value) +{ + m_settings.m_azimuth = value; + applySettings(); +} + +void GS232ControllerGUI::on_elevation_valueChanged(int value) +{ + m_settings.m_elevation = value; + applySettings(); +} + +void GS232ControllerGUI::on_track_stateChanged(int state) +{ + m_settings.m_track = state == Qt::Checked; + ui->devicesRefresh->setEnabled(m_settings.m_track); + ui->deviceLabel->setEnabled(m_settings.m_track); + ui->device->setEnabled(m_settings.m_track); + ui->channelLabel->setEnabled(m_settings.m_track); + ui->channel->setEnabled(m_settings.m_track); + applySettings(); +} + +void GS232ControllerGUI::updateStatus() +{ + int state = m_gs232Controller->getState(); + + if (m_lastFeatureState != state) + { + switch (state) + { + case Feature::StNotStarted: + ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case Feature::StIdle: + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"); + break; + case Feature::StRunning: + ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + break; + case Feature::StError: + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + QMessageBox::information(this, tr("Message"), m_gs232Controller->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } +} + +void GS232ControllerGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + GS232Controller::MsgConfigureGS232Controller* message = GS232Controller::MsgConfigureGS232Controller::create(m_settings, force); + m_gs232Controller->getInputMessageQueue()->push(message); + } +} diff --git a/plugins/feature/gs232controller/gs232controllergui.h b/plugins/feature/gs232controller/gs232controllergui.h new file mode 100644 index 000000000..a8a41f62c --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllergui.h @@ -0,0 +1,90 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GS232CONTROLLERGUI_H_ +#define INCLUDE_FEATURE_GS232CONTROLLERGUI_H_ + +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "gs232controllersettings.h" + +class PluginAPI; +class FeatureUISet; +class GS232Controller; + +namespace Ui { + class GS232ControllerGUI; +} + +class GS232ControllerGUI : public FeatureGUI { + Q_OBJECT +public: + static GS232ControllerGUI* 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::GS232ControllerGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + GS232ControllerSettings m_settings; + bool m_doApplySettings; + + GS232Controller* m_gs232Controller; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + + explicit GS232ControllerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~GS232ControllerGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void updateSerialPortList(); + void updateDeviceSetList(); + bool updateChannelList(); //!< true if channel index has changed + bool handleMessage(const Message& message); + + 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_devicesRefresh_clicked(); + void on_device_currentIndexChanged(int index); + void on_channel_currentIndexChanged(int index); + void on_serialPort_currentIndexChanged(int index); + void on_baudRate_currentIndexChanged(int index); + void on_track_stateChanged(int state); + void on_azimuth_valueChanged(int value); + void on_elevation_valueChanged(int value); + void updateStatus(); +}; + + +#endif // INCLUDE_FEATURE_GS232CONTROLLERGUI_H_ diff --git a/plugins/feature/gs232controller/gs232controllergui.ui b/plugins/feature/gs232controller/gs232controllergui.ui new file mode 100644 index 000000000..5189f070b --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllergui.ui @@ -0,0 +1,397 @@ + + + GS232ControllerGUI + + + + 0 + 0 + 320 + 187 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 320 + 16777215 + + + + + Liberation Sans + 9 + + + + GS-232 Rotator Controller + + + + + 10 + 10 + 301 + 171 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Azimuth + + + + + + + Target azimuth in degrees + + + 0 + + + 450 + + + 359 + + + + + + + + 23 + 0 + + + + Current azimuth in degrees + + + - + + + + + + + Qt::Vertical + + + + + + + Elevation + + + + + + + Target elevation in degrees + + + 180 + + + 180 + + + + + + + + 22 + 0 + + + + Current elevation in degrees + + + - + + + + + + + + + + + Serial Port + + + + + + + Name of serial port to use to connect to the GS-232 controller + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Baud rate + + + + + + + Serial port baud rate for the GS-232 controller + + + 3 + + + + 1200 + + + + + 2400 + + + + + 4800 + + + + + 9600 + + + + + 19200 + + + + + 38400 + + + + + 57600 + + + + + 115200 + + + + + + + + + + + + Check to enable automatic tracking of azimuth and elevation from the specified channel + + + Track + + + + + + + + 24 + 16777215 + + + + Refresh indexes of available device sets + + + + + + + :/recycle.png:/recycle.png + + + + + + + Device + + + + + + + + 55 + 0 + + + + Receiver deviceset index + + + + + + + Channel + + + + + + + + 55 + 0 + + + + Channel index + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+
+ + + + +
diff --git a/plugins/feature/gs232controller/gs232controllerplugin.cpp b/plugins/feature/gs232controller/gs232controllerplugin.cpp new file mode 100644 index 000000000..868d7a9df --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerplugin.cpp @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "gs232controllergui.h" +#endif +#include "gs232controller.h" +#include "gs232controllerplugin.h" +#include "gs232controllerwebapiadapter.h" + +const PluginDescriptor GS232ControllerPlugin::m_pluginDescriptor = { + GS232Controller::m_featureId, + QString("GS-232 Rotator Controller"), + QString("4.20.0"), + QString("(c) Jon Beniston, M7RCE"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +GS232ControllerPlugin::GS232ControllerPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& GS232ControllerPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void GS232ControllerPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(GS232Controller::m_featureIdURI, GS232Controller::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* GS232ControllerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* GS232ControllerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return GS232ControllerGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* GS232ControllerPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new GS232Controller(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* GS232ControllerPlugin::createFeatureWebAPIAdapter() const +{ + return new GS232ControllerWebAPIAdapter(); +} diff --git a/plugins/feature/gs232controller/gs232controllerplugin.h b/plugins/feature/gs232controller/gs232controllerplugin.h new file mode 100644 index 000000000..b58a51267 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GS232CONTROLLERPLUGIN_H +#define INCLUDE_FEATURE_GS232CONTROLLERPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class GS232ControllerPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.gs232controller") + +public: + explicit GS232ControllerPlugin(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_GS232CONTROLLERPLUGIN_H diff --git a/plugins/feature/gs232controller/gs232controllerreport.h b/plugins/feature/gs232controller/gs232controllerreport.h new file mode 100644 index 000000000..8e175d8fc --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerreport.h @@ -0,0 +1,64 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_GS232CONTROLLERREPORT_H_ +#define INCLUDE_FEATURE_GS232CONTROLLERREPORT_H_ + +#include + +#include "util/message.h" + +class GS232ControllerReport : public QObject +{ + Q_OBJECT +public: + enum AzAlType {TARGET, ACTUAL}; + + class MsgReportAzAl : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getAzimuth() const { return m_azimuth; } + float getElevation() const { return m_elevation; } + AzAlType getType() const { return m_type; } + + static MsgReportAzAl* create(float azimuth, float elevation, AzAlType type) + { + return new MsgReportAzAl(azimuth, elevation, type); + } + + private: + float m_azimuth; + float m_elevation; + AzAlType m_type; + + MsgReportAzAl(float azimuth, float elevation, AzAlType type) : + Message(), + m_azimuth(azimuth), + m_elevation(elevation), + m_type(type) + { + } + }; + +public: + GS232ControllerReport() {} + ~GS232ControllerReport() {} +}; + +#endif // INCLUDE_FEATURE_GS232CONTROLLERREPORT_H_ diff --git a/plugins/feature/gs232controller/gs232controllersettings.cpp b/plugins/feature/gs232controller/gs232controllersettings.cpp new file mode 100644 index 000000000..3b8ba8cb7 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllersettings.cpp @@ -0,0 +1,121 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "gs232controllersettings.h" + +MESSAGE_CLASS_DEFINITION(GS232ControllerSettings::MsgChannelIndexChange, Message) + +GS232ControllerSettings::GS232ControllerSettings() +{ + resetToDefaults(); +} + +void GS232ControllerSettings::resetToDefaults() +{ + m_azimuth = 0; + m_elevation = 0; + m_serialPort = ""; + m_baudRate = 9600; + m_track = false; + m_deviceIndex = -1; + m_channelIndex = -1; + m_title = "GS-232 Rotator Controller"; + 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 GS232ControllerSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_azimuth); + s.writeS32(2, m_elevation); + s.writeString(3, m_serialPort); + s.writeS32(4, m_baudRate); + s.writeBool(5, m_track); + s.writeS32(6, m_deviceIndex); + s.writeS32(7, m_channelIndex); + s.writeString(8, m_title); + s.writeU32(9, m_rgbColor); + s.writeBool(10, m_useReverseAPI); + s.writeString(11, m_reverseAPIAddress); + s.writeU32(12, m_reverseAPIPort); + s.writeU32(13, m_reverseAPIFeatureSetIndex); + s.writeU32(14, m_reverseAPIFeatureIndex); + + return s.final(); +} + +bool GS232ControllerSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + qint32 tmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_azimuth, 0); + d.readS32(2, &m_elevation, 0); + d.readString(3, &m_serialPort, ""); + d.readS32(4, &m_baudRate, 9600); + d.readBool(5, &m_track, false); + d.readS32(6, &m_deviceIndex, -1); + d.readS32(7, &m_channelIndex, -1); + d.readString(8, &m_title, "GS-232 Rotator Controller"); + d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb()); + d.readBool(10, &m_useReverseAPI, false); + d.readString(11, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(12, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(13, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(14, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/feature/gs232controller/gs232controllersettings.h b/plugins/feature/gs232controller/gs232controllersettings.h new file mode 100644 index 000000000..01cd25992 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllersettings.h @@ -0,0 +1,71 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GS232CONTROLLERSETTINGS_H_ +#define INCLUDE_FEATURE_GS232CONTROLLERSETTINGS_H_ + +#include +#include + +#include "util/message.h" + +class Serializable; + +struct GS232ControllerSettings +{ + class MsgChannelIndexChange : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getIndex() const { return m_index; } + + static MsgChannelIndexChange* create(int index) { + return new MsgChannelIndexChange(index); + } + + protected: + int m_index; + + MsgChannelIndexChange(int index) : + Message(), + m_index(index) + { } + }; + + int m_azimuth; + int m_elevation; + QString m_serialPort; + int m_baudRate; + int m_deviceIndex; + bool m_track; + int m_channelIndex; + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + + GS232ControllerSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif // INCLUDE_FEATURE_GS232CONTROLLERSETTINGS_H_ diff --git a/plugins/feature/gs232controller/gs232controllerwebapiadapter.cpp b/plugins/feature/gs232controller/gs232controllerwebapiadapter.cpp new file mode 100644 index 000000000..2bfd5de86 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerwebapiadapter.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "gs232controller.h" +#include "gs232controllerwebapiadapter.h" + +GS232ControllerWebAPIAdapter::GS232ControllerWebAPIAdapter() +{} + +GS232ControllerWebAPIAdapter::~GS232ControllerWebAPIAdapter() +{} + +int GS232ControllerWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings()); + response.getSimplePttSettings()->init(); + GS232Controller::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int GS232ControllerWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + GS232Controller::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/gs232controller/gs232controllerwebapiadapter.h b/plugins/feature/gs232controller/gs232controllerwebapiadapter.h new file mode 100644 index 000000000..7afa55e63 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GS232CONTROLLER_WEBAPIADAPTER_H +#define INCLUDE_GS232CONTROLLER_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "gs232controllersettings.h" + +/** + * Standalone API adapter only for the settings + */ +class GS232ControllerWebAPIAdapter : public FeatureWebAPIAdapter { +public: + GS232ControllerWebAPIAdapter(); + virtual ~GS232ControllerWebAPIAdapter(); + + 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: + GS232ControllerSettings m_settings; +}; + +#endif // INCLUDE_GS232CONTROLLER_WEBAPIADAPTER_H diff --git a/plugins/feature/gs232controller/gs232controllerworker.cpp b/plugins/feature/gs232controller/gs232controllerworker.cpp new file mode 100644 index 000000000..59af9333b --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerworker.cpp @@ -0,0 +1,248 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "SWGDeviceState.h" +#include "SWGSuccessResponse.h" +#include "SWGErrorResponse.h" +#include "SWGDeviceSettings.h" +#include "SWGChannelSettings.h" +#include "SWGDeviceSet.h" +#include "SWGChannelReport.h" + +#include "webapi/webapiadapterinterface.h" +#include "webapi/webapiutils.h" + +#include "gs232controllerworker.h" +#include "gs232controllerreport.h" + +MESSAGE_CLASS_DEFINITION(GS232ControllerWorker::MsgConfigureGS232ControllerWorker, Message) +MESSAGE_CLASS_DEFINITION(GS232ControllerReport::MsgReportAzAl, Message) + +GS232ControllerWorker::GS232ControllerWorker(WebAPIAdapterInterface *webAPIAdapterInterface) : + m_webAPIAdapterInterface(webAPIAdapterInterface), + m_msgQueueToFeature(nullptr), + m_msgQueueToGUI(nullptr), + m_running(false), + m_mutex(QMutex::Recursive) +{ + connect(&m_pollTimer, SIGNAL(timeout()), this, SLOT(update())); + m_pollTimer.start(1000); +} + +GS232ControllerWorker::~GS232ControllerWorker() +{ + m_inputMessageQueue.clear(); +} + +void GS232ControllerWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +bool GS232ControllerWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + connect(&m_serialPort, &QSerialPort::readyRead, this, &GS232ControllerWorker::readSerialData); + m_running = true; + return m_running; +} + +void GS232ControllerWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + disconnect(&m_serialPort, &QSerialPort::readyRead, this, &GS232ControllerWorker::readSerialData); + m_running = false; +} + +void GS232ControllerWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool GS232ControllerWorker::handleMessage(const Message& cmd) +{ + if (MsgConfigureGS232ControllerWorker::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureGS232ControllerWorker& cfg = (MsgConfigureGS232ControllerWorker&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else + { + return false; + } +} + +void GS232ControllerWorker::applySettings(const GS232ControllerSettings& settings, bool force) +{ + qDebug() << "GS232ControllerWorker::applySettings:" + << " m_azimuth: " << settings.m_azimuth + << " m_elevation: " << settings.m_elevation + << " m_serialPort: " << settings.m_serialPort + << " m_baudRate: " << settings.m_baudRate + << " m_deviceIndex: " << settings.m_deviceIndex + << " m_channelIndex: " << settings.m_channelIndex + << " force: " << force; + + if ((settings.m_serialPort != m_settings.m_serialPort) || force) + { + if (m_serialPort.isOpen()) + m_serialPort.close(); + m_serialPort.setPortName(settings.m_serialPort); + m_serialPort.setBaudRate(settings.m_baudRate); + if (!m_serialPort.open(QIODevice::ReadWrite)) + qCritical() << "GS232ControllerWorker::applySettings: Failed to open serial port " << settings.m_serialPort << ". Error: " << m_serialPort.error(); + } + else if ((settings.m_baudRate != m_settings.m_baudRate) || force) + { + m_serialPort.setBaudRate(settings.m_baudRate); + } + + if ((settings.m_elevation != m_settings.m_elevation) || force) + { + setAzimuthElevation(settings.m_azimuth, settings.m_elevation); + } + else if ((settings.m_azimuth != m_settings.m_azimuth) || force) + { + setAzimuth(settings.m_azimuth); + } + + m_settings = settings; +} + +void GS232ControllerWorker::setAzimuth(int azimuth) +{ + QString cmd = QString("M%1\r\n").arg(azimuth, 3, 10, QLatin1Char('0')); + QByteArray data = cmd.toLatin1(); + m_serialPort.write(data); +} + +void GS232ControllerWorker::setAzimuthElevation(int azimuth, int elevation) +{ + QString cmd = QString("W%1 %2\r\n").arg(azimuth, 3, 10, QLatin1Char('0')).arg(elevation, 3, 10, QLatin1Char('0')); + QByteArray data = cmd.toLatin1(); + m_serialPort.write(data); +} + +void GS232ControllerWorker::readSerialData() +{ + char buf[1024]; + qint64 len; + + while (m_serialPort.canReadLine()) + { + len = m_serialPort.readLine(buf, sizeof(buf)); + if (len != -1) + { + QString response = QString::fromUtf8(buf, len); + + QRegularExpression re("AZ=(\\d\\d\\d)EL=(\\d\\d\\d)"); + QRegularExpressionMatch match = re.match(response); + if (match.hasMatch()) + { + QString az = match.captured(1); + QString el = match.captured(2); + //qDebug() << "GS232ControllerWorker::readSerialData read az " << az << " el " << el; + if (getMessageQueueToGUI()) + { + GS232ControllerReport::MsgReportAzAl *msg = GS232ControllerReport::MsgReportAzAl::create( + az.toFloat(), el.toFloat(), GS232ControllerReport::ACTUAL); + getMessageQueueToGUI()->push(msg); + } + } + else + { + qDebug() << "GS232ControllerWorker::readSerialData unknown response " << response; + } + } + } +} + +void GS232ControllerWorker::update() +{ + // Request current Az/El from GS-232 controller + if (m_serialPort.isOpen()) + { + QByteArray cmd("C2\r\n"); + m_serialPort.write(cmd); + } + + // Request target Az/EL from channel + if (m_settings.m_track) + { + SWGSDRangel::SWGChannelReport response; + SWGSDRangel::SWGErrorResponse errorResponse; + + int httpRC = m_webAPIAdapterInterface->devicesetChannelReportGet( + m_settings.m_deviceIndex, + m_settings.m_channelIndex, + response, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("GS232ControllerWorker::update: get channel report error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + } + else + { + QJsonObject *jsonObj = response.asJsonObject(); + double targetAzimuth; + double targetElevation; + bool gotElevation = false; + bool gotAzimuth = false; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "targetAzimuth", targetAzimuth)) + gotAzimuth = true; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "targetElevation", targetElevation)) + gotElevation = true; + + if (gotAzimuth && gotElevation) + { + if (getMessageQueueToGUI()) + { + GS232ControllerReport::MsgReportAzAl *msg = GS232ControllerReport::MsgReportAzAl::create( + targetAzimuth, targetElevation, GS232ControllerReport::TARGET); + getMessageQueueToGUI()->push(msg); + } + } + } + } +} diff --git a/plugins/feature/gs232controller/gs232controllerworker.h b/plugins/feature/gs232controller/gs232controllerworker.h new file mode 100644 index 000000000..2041e17c8 --- /dev/null +++ b/plugins/feature/gs232controller/gs232controllerworker.h @@ -0,0 +1,94 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GS232CONTROLLERWORKER_H_ +#define INCLUDE_FEATURE_GS232CONTROLLERWORKER_H_ + +#include +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" + +#include "gs232controllersettings.h" + +class WebAPIAdapterInterface; + +class GS232ControllerWorker : public QObject +{ + Q_OBJECT +public: + class MsgConfigureGS232ControllerWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const GS232ControllerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureGS232ControllerWorker* create(const GS232ControllerSettings& settings, bool force) + { + return new MsgConfigureGS232ControllerWorker(settings, force); + } + + private: + GS232ControllerSettings m_settings; + bool m_force; + + MsgConfigureGS232ControllerWorker(const GS232ControllerSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + GS232ControllerWorker(WebAPIAdapterInterface *webAPIAdapterInterface); + ~GS232ControllerWorker(); + 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: + + 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; + GS232ControllerSettings m_settings; + bool m_running; + QMutex m_mutex; + QSerialPort m_serialPort; + QTimer m_pollTimer; + + bool handleMessage(const Message& cmd); + void applySettings(const GS232ControllerSettings& settings, bool force = false); + MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; } + void setAzimuth(int azimuth); + void setAzimuthElevation(int azimuth, int elevation); + +private slots: + void handleInputMessages(); + void readSerialData(); + void update(); +}; + +#endif // INCLUDE_FEATURE_GS232CONTROLLERWORKER_H_ diff --git a/plugins/feature/gs232controller/readme.md b/plugins/feature/gs232controller/readme.md new file mode 100644 index 000000000..3e55f92ea --- /dev/null +++ b/plugins/feature/gs232controller/readme.md @@ -0,0 +1,52 @@ +

GS-232 Rotator Controller Feature Plugin

+ +

Introduction

+ +The GS-232 Rotator Controller feature plugin allows SDRangel to send commands to GS-232 rotators. This allows SDRangel to point antennas mounted on a rotator to a specified azimuth and elevation. + +Azimuth and elevation can be set manually by a user in the GUI, via the REST API, or via another plugin, such as the ADS-B Demodulator, which can track a selected aircraft. + +

Interface

+ +![File source channel plugin GUI](../../../doc/img/GS232Controller_plugin.png) + +

1: Start/Stop plugin

+ +This button starts or stops the plugin. When the plugin is stopped, azimuth and elevation commands will not be sent to the GS-232 rotator. + +

2: Azimuth

+ +Specifies the target azimuth (angle in degrees, clockwise from North) to point the antenna towards. Valid values range from 0 to 450 degrees. The value to the right of the target azimuth, is the current azimuth read from the GS-232 rotator. + +

3: Elevation

+ +Specifies the target elevation (angle in degrees) to point the antenna towards. Valid values range from 0 to 180 degrees, where 0 and 180 point towards the horizon and 90 degrees to zenith. The value to the right of the target elevation, is the current elevation read from the GS-232 rotator. + +

4: Serial Port

+ +Specifies the serial port (E.g. COM3 on Windows or /dev/ttyS0 on Linux) that will be used to send commands to the GS-232 rotator. + +

5: Baud rate

+ +Specifies the baud rate that will be used to send commands to the GS-232 rotator. Typically this is 9600. + +

6: Track

+ +When checked, the GS-232 Rotator Controller plugin will query the channel specified by the Device (8) and Channel (9) combo boxes for the target azimuth and elevation. For example, this allows an aircraft to be tracked, by setting the Device and Channel to correspond to the ADS-B Demodulator plugin. + +

7: Refresh list of devices and channels

+ +Use this button to refresh the list of devices (8) and channels (9) + +

8: Select device set

+ +Specify the SDRangel device set containing the channel plugin that will be asked for aziumth and elevation values. Defaults to R0. + +

9: Select channel

+ +The channel index specifies the SDRangel channel that will be asked for azimuth and elevation values. Defaults to 0. + +

Developer Information

+ +For a channel plugin to be able to set the azimuth and elevation, its channel report should contain targetAzimuth and targetElevation. See the ADS-B plugin as an example. + \ No newline at end of file diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 407e8d1f2..aeac9d603 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -149,6 +149,7 @@ set(sdrbase_SOURCES settings/preset.cpp settings/mainsettings.cpp + util/azel.cpp util/crc.cpp util/CRC64.cpp util/db.cpp @@ -314,6 +315,7 @@ set(sdrbase_HEADERS settings/preset.h settings/mainsettings.h + util/azel.h util/CRC64.h util/db.h util/doublebuffer.h diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 2dd81cdeb..abbdba250 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -696,6 +696,87 @@ margin-bottom: 20px;