diff --git a/CMakeLists.txt b/CMakeLists.txt index 34e5c3761..18281ed8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ option(ENABLE_SOAPYSDR "Enable SoapySDR support" ON) option(ENABLE_XTRX "Enable XTRX support" ON) option(ENABLE_PACK_MIRSDRAPI "Enable inclusion of the mirsdr-api library - for personal use only" OFF) option(ENABLE_USRP "Enable USRP support" ON) +option(ENABLE_FOBOS "Enable Fobos SDR support" ON) # Channel Rx enablers option(ENABLE_CHANNELRX "Enable channelrx plugins" ON) @@ -416,6 +417,16 @@ elseif (WIN32) set(LIBUSB_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/libusb/MS64/dll/libusb-1.0.lib" CACHE INTERNAL "") set(LIBUSB_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/libusb/MS64/dll" CACHE INTERNAL "") + set(FOBOS_SDR_FOUND ON CACHE INTERNAL "") + set(FOBOS_SDR_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/fobos-sdr/include" CACHE INTERNAL "") + set(FOBOS_SDR_LIBRARY "${EXTERNAL_LIBRARY_FOLDER}/fobos-sdr/lib/fobos_sdr.lib" CACHE INTERNAL "") + set(FOBOS_SDR_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/fobos-sdr/bin" CACHE INTERNAL "") + + set(FOBOS_REGULAR_FOUND ON CACHE INTERNAL "") + set(FOBOS_REGULAR_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/fobos-regular/include" CACHE INTERNAL "") + set(FOBOS_REGULAR_LIBRARY "${EXTERNAL_LIBRARY_FOLDER}/fobos-regular/lib/fobos.lib" CACHE INTERNAL "") + set(FOBOS_REGULAR_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/fobos-regular/bin" CACHE INTERNAL "") + if(VS2022 OR VS2019) set(OpenCV_DIR "${EXTERNAL_LIBRARY_FOLDER}/opencv4" CACHE INTERNAL "") else() @@ -493,6 +504,7 @@ elseif (WIN32) "${EXTERNAL_LIBRARY_FOLDER}/libusb/MS64/dll" "${EXTERNAL_LIBRARY_FOLDER}/ffmpeg/bin" "${EXTERNAL_LIBRARY_FOLDER}/libsigmf/lib" + "${EXTERNAL_LIBRARY_FOLDER}/fobos-sdr/bin" ) elseif(ANDROID) set(EXTERNAL_LIBRARY_FOLDER "${CMAKE_SOURCE_DIR}/external/android") diff --git a/external/windows b/external/windows index eddef8183..72317cca9 160000 --- a/external/windows +++ b/external/windows @@ -1 +1 @@ -Subproject commit eddef8183524e5aaef978088427d4b22a5afb393 +Subproject commit 72317cca9b2362b8acfd68efcd0d7ad169ce1b7d diff --git a/plugins/samplesource/CMakeLists.txt b/plugins/samplesource/CMakeLists.txt index 92cf8d85b..15c150253 100644 --- a/plugins/samplesource/CMakeLists.txt +++ b/plugins/samplesource/CMakeLists.txt @@ -4,6 +4,12 @@ add_subdirectory(fileinput) add_subdirectory(testsource) add_subdirectory(localinput) +if(ENABLE_FOBOS AND WIN32) + add_subdirectory(fobos) +else() + message(STATUS "Not building fobos input (ENABLE_FOBOS=${ENABLE_FOBOS} WIN32=${WIN32})") +endif() + if (CM256CC_FOUND AND (HAS_SSE3 OR HAS_NEON)) add_subdirectory(remoteinput) else() diff --git a/plugins/samplesource/fobos/CMakeLists.txt b/plugins/samplesource/fobos/CMakeLists.txt new file mode 100644 index 000000000..7f063752b --- /dev/null +++ b/plugins/samplesource/fobos/CMakeLists.txt @@ -0,0 +1,124 @@ +project(fobos) + +option(FOBOS_DEBUG_FILE_LOG "Write Fobos SDR diagnostic log file" OFF) + +# Fobos SDR runtime packages. On Windows official builds these are expected +# to come from external/windows/fobos-sdr and external/windows/fobos-regular +# in the SDRangel Windows dependency repo. +if(WIN32) + set(FOBOS_SDR_INCLUDE_DIR "${FOBOS_SDR_INCLUDE_DIR}" CACHE PATH "Fobos SDR Agile include directory") + set(FOBOS_SDR_LIBRARY "${FOBOS_SDR_LIBRARY}" CACHE FILEPATH "Fobos SDR Agile import library") + set(FOBOS_SDR_DLL_DIR "${FOBOS_SDR_DLL_DIR}" CACHE PATH "Fobos SDR Agile runtime DLL directory") + set(FOBOS_REGULAR_INCLUDE_DIR "${FOBOS_REGULAR_INCLUDE_DIR}" CACHE PATH "Fobos SDR regular include directory") + set(FOBOS_REGULAR_LIBRARY "${FOBOS_REGULAR_LIBRARY}" CACHE FILEPATH "Fobos SDR regular import library") + set(FOBOS_REGULAR_DLL_DIR "${FOBOS_REGULAR_DLL_DIR}" CACHE PATH "Fobos SDR regular runtime DLL directory") + + if(NOT EXISTS "${FOBOS_SDR_INCLUDE_DIR}/fobos_sdr.h") + message(FATAL_ERROR "Fobos SDR Agile header not found: ${FOBOS_SDR_INCLUDE_DIR}/fobos_sdr.h") + endif() + if(NOT EXISTS "${FOBOS_SDR_DLL_DIR}/fobos_sdr.dll") + message(FATAL_ERROR "Fobos SDR Agile runtime DLL not found: ${FOBOS_SDR_DLL_DIR}/fobos_sdr.dll") + endif() + if(NOT EXISTS "${FOBOS_REGULAR_INCLUDE_DIR}/fobos.h") + message(FATAL_ERROR "Fobos SDR regular header not found: ${FOBOS_REGULAR_INCLUDE_DIR}/fobos.h") + endif() + if(NOT EXISTS "${FOBOS_REGULAR_DLL_DIR}/fobos.dll") + message(FATAL_ERROR "Fobos SDR regular runtime DLL not found: ${FOBOS_REGULAR_DLL_DIR}/fobos.dll") + endif() +endif() + +set(FOBOS_SOURCES + fobosinput.cpp + fobosplugin.cpp + fobosworker.cpp + fobossettings.cpp + foboswebapiadapter.cpp +) + +set(FOBOS_HEADERS + fobosinput.h + fobosplugin.h + fobosworker.h + fobossettings.h + foboswebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(FOBOS_SOURCES + ${FOBOS_SOURCES} + fobosgui.cpp + fobosgui.ui + ) + set(FOBOS_HEADERS + ${FOBOS_HEADERS} + fobosgui.h + ) + + set(TARGET_NAME ${PLUGINS_PREFIX}inputFOBOS) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME ${PLUGINSSRV_PREFIX}inputFOBOSsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +if(NOT Qt6_FOUND) + add_library(${TARGET_NAME} ${FOBOS_SOURCES}) +else() + qt_add_plugin(${TARGET_NAME} CLASS_NAME FOBOSPlugin) + target_sources(${TARGET_NAME} PRIVATE ${FOBOS_SOURCES} ${FOBOS_HEADERS}) +endif() + +if(NOT BUILD_SHARED_LIBS) + set_property(GLOBAL APPEND PROPERTY STATIC_PLUGINS_PROPERTY ${TARGET_NAME}) +endif() + +if(WIN32) + target_include_directories(${TARGET_NAME} PRIVATE ${FOBOS_SDR_INCLUDE_DIR} ${FOBOS_REGULAR_INCLUDE_DIR}) +endif() + +target_link_libraries(${TARGET_NAME} PRIVATE + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger +) + +if(FOBOS_DEBUG_FILE_LOG) + target_compile_definitions(${TARGET_NAME} PRIVATE FOBOS_DEBUG_FILE_LOG) +endif() + +if(WIN32) + # Make developer builds runnable without manually copying Fobos runtime files. + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FOBOS_SDR_DLL_DIR}/fobos_sdr.dll" + "${SDRANGEL_BINARY_BIN_DIR}/fobos_sdr.dll" + COMMENT "Copying Fobos SDR Agile runtime DLL" + ) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FOBOS_REGULAR_DLL_DIR}/fobos.dll" + "${SDRANGEL_BINARY_BIN_DIR}/fobos.dll" + COMMENT "Copying Fobos SDR regular runtime DLL" + ) + + + install(FILES "${FOBOS_SDR_DLL_DIR}/fobos_sdr.dll" DESTINATION ${INSTALL_BIN_DIR}) + install(FILES "${FOBOS_REGULAR_DLL_DIR}/fobos.dll" DESTINATION ${INSTALL_BIN_DIR}) +endif() + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + install(FILES $/${TARGET_NAME}stripped.pdb CONFIGURATIONS Release DESTINATION ${INSTALL_FOLDER} RENAME ${TARGET_NAME}.pdb) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER}) +endif() diff --git a/plugins/samplesource/fobos/fobosgui.cpp b/plugins/samplesource/fobos/fobosgui.cpp new file mode 100644 index 000000000..f682e5bbf --- /dev/null +++ b/plugins/samplesource/fobos/fobosgui.cpp @@ -0,0 +1,930 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2022-2023 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui_FOBOSgui.h" +#include "gui/colormapper.h" +#include "gui/glspectrum.h" +#include "gui/basicdevicesettingsdialog.h" +#include "gui/dialpopup.h" +#include "gui/dialogpositioner.h" +#include "mainspectrum/mainspectrumgui.h" +#include "dsp/dspcommands.h" +#include "util/db.h" + +#include "FOBOSgui.h" +#include "device/deviceapi.h" +#include "device/deviceuiset.h" + +FOBOSGui::FOBOSGui(DeviceUISet *deviceUISet, QWidget* parent) : + DeviceGUI(parent), + ui(new Ui::FOBOSGui), + m_settings(), + m_doApplySettings(true), + m_forceSettings(true), + m_sampleSource(0), + m_tickCount(0), + m_lastEngineState(DeviceAPI::StNotStarted), + m_backendStatus(QStringLiteral("Not started")), + m_backendDetails(QStringLiteral("Backend mode: Auto. Start the source to detect Agile or Regular API.")) +{ + qDebug("FOBOSGui::FOBOSGui"); + m_deviceUISet = deviceUISet; + setAttribute(Qt::WA_DeleteOnClose, true); + m_sampleSource = m_deviceUISet->m_deviceAPI->getSampleSource(); + + ui->setupUi(getContents()); + sizeToContents(); + getContents()->setStyleSheet("#FOBOSGui { background-color: rgb(64, 64, 64); }"); + m_helpURL = "plugins/samplesource/FOBOS/readme.md"; + ui->centerFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->centerFrequency->setValueRange(9, 0, 999999999); + ui->sampleRate->setColorMapper(ColorMapper(ColorMapper::GrayGreenYellow)); + ui->sampleRate->setValueRange(8, 8000000, 50000000); + ui->frequencyShift->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->frequencyShift->setValueRange(false, 7, -9999999, 9999999); + ui->frequencyShiftLabel->setText(QString("%1").arg(QChar(0x94, 0x03))); + + // Fobos SDR: Auto backend selection with controlled restart, live gain control and device-busy diagnostics. + // Device stop/start lifecycle is handled by FOBOSWorker. + ui->autoCorrLabel->setText(QStringLiteral("LNA")); + ui->autoCorr->clear(); + ui->autoCorr->addItem(QStringLiteral("0")); + ui->autoCorr->addItem(QStringLiteral("1")); + ui->autoCorr->addItem(QStringLiteral("2")); + + ui->modulationLabel->setText(QStringLiteral("VGA")); + ui->modulation->clear(); + for (int i = 0; i <= 31; ++i) { + ui->modulation->addItem(QString::number(i)); + } + + ui->samplerateLabel->setText(QStringLiteral("SR")); + ui->sampleRateUnit->setText(QStringLiteral("S/s")); + ui->amplitudeCoarseLabel->setText(QStringLiteral("IQ scale coarse")); + ui->amplitudeFineLabel->setText(QStringLiteral("IQ scale fine")); + + // Hide TestSource-only controls that are not part of the real Fobos source. + // Planned uSDR-style controls for later sub-iterations: Input RF/HF modes, BW %, GPO, External clock. + ui->decimationLabel->setVisible(false); + ui->decimation->setVisible(false); + ui->fcPosLabel->setVisible(false); + ui->fcPos->setVisible(false); + ui->sampleSzieLabel->setVisible(false); + ui->sampleSize->setVisible(false); + ui->sampleSizeUnits->setVisible(false); + ui->frequencyShiftLabel->setVisible(false); + ui->frequencyShift->setVisible(false); + ui->frequencyShiftUnits->setVisible(false); + ui->modulationFrequency->setVisible(false); + ui->modulationFrequencyText->setVisible(false); + ui->amModulationLabel->setVisible(false); + ui->amModulation->setVisible(false); + ui->amModulationText->setVisible(false); + ui->fmDeviationLabel->setVisible(false); + ui->fmDeviation->setVisible(false); + ui->fmDeviationText->setVisible(false); + ui->dcBiasLabel->setVisible(false); + ui->dcBiasMinusLabel->setVisible(false); + ui->dcBias->setVisible(false); + ui->dcBiasPlusLabel->setVisible(false); + ui->dcBiasText->setVisible(false); + ui->iBiasLabel->setVisible(false); + ui->iBiasMinusLabel->setVisible(false); + ui->iBias->setVisible(false); + ui->iBiasText->setVisible(false); + ui->iBiasPlusLabel->setVisible(false); + ui->qBiasLabel->setVisible(false); + ui->qBiasMinusLabel->setVisible(false); + ui->qBias->setVisible(false); + ui->qBiasText->setVisible(false); + ui->qBiasPlusQLabel->setVisible(false); + ui->phaseImbalanceLabel->setVisible(false); + ui->phaseImbalanceMinusLabel->setVisible(false); + ui->phaseImbalance->setVisible(false); + ui->phaseImbalanceText->setVisible(false); + ui->phaseImbalancePlusLabel->setVisible(false); + ui->line->setVisible(false); + ui->line_4->setVisible(false); + + ui->power->setText(QStringLiteral("Stopped")); + ui->autoCorr->setToolTip(QStringLiteral("Fobos LNA gain 0..2")); + ui->modulation->setToolTip(QStringLiteral("Fobos VGA gain 0..31")); + ui->sampleRate->setToolTip(QStringLiteral("Fobos sample rate in S/s. Supported rates are read from API on Start. Changing sample rate while running requires restart.")); + ui->amplitudeCoarse->setToolTip(QStringLiteral("Digital IQ display scale coarse: value/100. Live software display gain.")); + ui->amplitudeFine->setToolTip(QStringLiteral("Digital IQ display scale fine: value/100. Live software display gain.")); + ui->centerFrequency->setToolTip(QStringLiteral("Center frequency. Live retune is supported in RF mode.")); + ui->power->setToolTip(QStringLiteral("Fobos source status.")); + + displaySettings(); + makeUIConnections(); + + connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateHardware())); + connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); + m_statusTimer.start(500); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()), Qt::QueuedConnection); + m_sampleSource->setMessageQueueToGUI(&m_inputMessageQueue); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(openDeviceSettingsDialog(const QPoint &))); + DialPopup::addPopupsToChildDials(this); + m_resizer.enableChildMouseTracking(); +} + +FOBOSGui::~FOBOSGui() +{ + m_statusTimer.stop(); + m_updateTimer.stop(); + delete ui; +} + +void FOBOSGui::destroy() +{ + delete this; +} + +void FOBOSGui::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + m_forceSettings = true; + sendSettings(); +} + +QByteArray FOBOSGui::serialize() const +{ + return m_settings.serialize(); +} + +bool FOBOSGui::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + m_forceSettings = true; + sendSettings(); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void FOBOSGui::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + FOBOSInput::MsgStartStop *message = FOBOSInput::MsgStartStop::create(checked); + m_sampleSource->getInputMessageQueue()->push(message); + } +} + +void FOBOSGui::on_centerFrequency_changed(quint64 value) +{ + m_settings.m_centerFrequency = value * 1000; + m_settingsKeys.append("centerFrequency"); + sendSettings(); +} + +void FOBOSGui::on_autoCorr_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 2)) { + return; + } + + m_settings.m_autoCorrOptions = static_cast(index); + m_settingsKeys.append("autoCorrOptions"); + sendSettings(); +} + +void FOBOSGui::on_frequencyShift_changed(qint64 value) +{ + m_settings.m_frequencyShift = value; + m_settingsKeys.append("frequencyShift"); + sendSettings(); +} + +void FOBOSGui::on_decimation_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 6)) { + return; + } + + m_settings.m_log2Decim = index; + m_settingsKeys.append("log2Decim"); + sendSettings(); +} + +void FOBOSGui::on_fcPos_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 2)) { + return; + } + + m_settings.m_fcPos = (FOBOSSettings::fcPos_t) index; + m_settingsKeys.append("fcPos"); + sendSettings(); +} + +void FOBOSGui::on_sampleRate_changed(quint64 value) +{ + updateFrequencyShiftLimit(); + m_settings.m_frequencyShift = ui->frequencyShift->getValueNew(); + m_settings.m_sampleRate = value; + m_settingsKeys.append("frequencyShift"); + m_settingsKeys.append("sampleRate"); + sendSettings(); +} + +void FOBOSGui::on_sampleSize_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 2)) { + return; + } + + updateAmpCoarseLimit(); + updateAmpFineLimit(); + displayAmplitude(); + m_settings.m_amplitudeBits = ui->amplitudeCoarse->value() * 100 + ui->amplitudeFine->value(); + m_settings.m_sampleSizeIndex = index; + m_settingsKeys.append("amplitudeBits"); + m_settingsKeys.append("sampleSizeIndex"); + sendSettings(); +} + +void FOBOSGui::on_amplitudeCoarse_valueChanged(int value) +{ + (void) value; + updateAmpFineLimit(); + displayAmplitude(); + m_settings.m_amplitudeBits = ui->amplitudeCoarse->value() * 100 + ui->amplitudeFine->value(); + m_settingsKeys.append("amplitudeBits"); + sendSettings(); +} + +void FOBOSGui::on_amplitudeFine_valueChanged(int value) +{ + (void) value; + displayAmplitude(); + m_settings.m_amplitudeBits = ui->amplitudeCoarse->value() * 100 + ui->amplitudeFine->value(); + m_settingsKeys.append("amplitudeBits"); + sendSettings(); +} + +void FOBOSGui::on_modulation_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 31)) { + return; + } + + m_settings.m_modulation = static_cast(index); + m_settingsKeys.append("modulation"); + sendSettings(); +} + +void FOBOSGui::on_modulationFrequency_valueChanged(int value) +{ + m_settings.m_modulationTone = value; + ui->modulationFrequencyText->setText(QString("%1").arg(m_settings.m_modulationTone / 100.0, 0, 'f', 2)); + m_settingsKeys.append("modulationTone"); + sendSettings(); +} + +void FOBOSGui::on_amModulation_valueChanged(int value) +{ + m_settings.m_amModulation = value; + ui->amModulationText->setText(QString("%1").arg(m_settings.m_amModulation)); + m_settingsKeys.append("amModulation"); + sendSettings(); +} + +void FOBOSGui::on_fmDeviation_valueChanged(int value) +{ + m_settings.m_fmDeviation = value; + ui->fmDeviationText->setText(QString("%1").arg(m_settings.m_fmDeviation / 10.0, 0, 'f', 1)); + m_settingsKeys.append("fmDeviation"); + sendSettings(); +} + +void FOBOSGui::on_dcBias_valueChanged(int value) +{ + ui->dcBiasText->setText(QString(tr("%1 %").arg(value))); + m_settings.m_dcFactor = value / 100.0f; + m_settingsKeys.append("dcFactor"); + sendSettings(); +} + +void FOBOSGui::on_iBias_valueChanged(int value) +{ + ui->iBiasText->setText(QString(tr("%1 %").arg(value))); + m_settings.m_iFactor = value / 100.0f; + m_settingsKeys.append("iFactor"); + sendSettings(); +} + +void FOBOSGui::on_qBias_valueChanged(int value) +{ + ui->qBiasText->setText(QString(tr("%1 %").arg(value))); + m_settings.m_qFactor = value / 100.0f; + m_settingsKeys.append("qFactor"); + sendSettings(); +} + +void FOBOSGui::on_phaseImbalance_valueChanged(int value) +{ + ui->phaseImbalanceText->setText(QString(tr("%1 %").arg(value))); + m_settings.m_phaseImbalance = value / 100.0f; + m_settingsKeys.append("phaseImbalance"); + sendSettings(); +} + +void FOBOSGui::displayAmplitude() +{ + int amplitudeInt = ui->amplitudeCoarse->value() * 100 + ui->amplitudeFine->value(); + double power; + + switch (ui->sampleSize->currentIndex()) + { + case 0: // 8 bits: 128 + power = (double) amplitudeInt*amplitudeInt / (double) (1<<14); + break; + case 1: // 12 bits 2048 + power = (double) amplitudeInt*amplitudeInt / (double) (1<<22); + break; + case 2: // 16 bits 32768 + default: + power = (double) amplitudeInt*amplitudeInt / (double) (1<<30); + break; + } + + const double iqGain = amplitudeInt / 100.0; + ui->amplitudeBits->setText(QString(tr("x%1").arg(QString::number(iqGain, 'f', 2)))); + (void) power; + // Fobos SDR: ui->power is reserved for device status. + // Do not overwrite Running/Stopped/Error with a legacy TestSource dB display. + ui->power->setToolTip(QString(tr("Digital IQ display scale x%1. This is software display gain, not RF gain.")) + .arg(QString::number(iqGain, 'f', 2))); +} + +void FOBOSGui::updateAmpCoarseLimit() +{ + switch (ui->sampleSize->currentIndex()) + { + case 0: // 8 bits: 128 + ui->amplitudeCoarse->setMaximum(1); + break; + case 1: // 12 bits 2048 + ui->amplitudeCoarse->setMaximum(20); + break; + case 2: // 16 bits 32768 + default: + ui->amplitudeCoarse->setMaximum(327); + break; + } +} + +void FOBOSGui::updateAmpFineLimit() +{ + switch (ui->sampleSize->currentIndex()) + { + case 0: // 8 bits: 128 + if (ui->amplitudeCoarse->value() == 1) { + ui->amplitudeFine->setMaximum(27); + } else { + ui->amplitudeFine->setMaximum(99); + } + break; + case 1: // 12 bits 2048 + if (ui->amplitudeCoarse->value() == 20) { + ui->amplitudeFine->setMaximum(47); + } else { + ui->amplitudeFine->setMaximum(99); + } + break; + case 2: // 16 bits 32768 + default: + if (ui->amplitudeCoarse->value() == 327) { + ui->amplitudeFine->setMaximum(67); + } else { + ui->amplitudeFine->setMaximum(99); + } + break; + } +} + +void FOBOSGui::updateFrequencyShiftLimit() +{ + int sampleRate = ui->sampleRate->getValueNew(); + ui->frequencyShift->setValueRange(false, 7, -sampleRate, sampleRate); +} + +void FOBOSGui::displaySettings() +{ + blockApplySettings(true); + ui->sampleSize->blockSignals(true); + + setTitle(m_settings.m_title); + getDeviceUISet()->m_mainSpectrumGUI->setTitle(m_settings.m_title); + ui->centerFrequency->setValue(m_settings.m_centerFrequency / 1000); + ui->decimation->setCurrentIndex(m_settings.m_log2Decim); + ui->fcPos->setCurrentIndex((int) m_settings.m_fcPos); + ui->sampleRate->setValue(m_settings.m_sampleRate); + updateFrequencyShiftLimit(); + ui->frequencyShift->setValue(m_settings.m_frequencyShift); + ui->sampleSize->setCurrentIndex(m_settings.m_sampleSizeIndex); + updateAmpCoarseLimit(); + int amplitudeBits = m_settings.m_amplitudeBits; + ui->amplitudeCoarse->setValue(amplitudeBits/100); + updateAmpFineLimit(); + ui->amplitudeFine->setValue(amplitudeBits%100); + displayAmplitude(); + int dcBiasPercent = roundf(m_settings.m_dcFactor * 100.0f); + ui->dcBias->setValue((int) dcBiasPercent); + ui->dcBiasText->setText(QString(tr("%1 %").arg(dcBiasPercent))); + int iBiasPercent = roundf(m_settings.m_iFactor * 100.0f); + ui->iBias->setValue((int) iBiasPercent); + ui->iBiasText->setText(QString(tr("%1 %").arg(iBiasPercent))); + int qBiasPercent = roundf(m_settings.m_qFactor * 100.0f); + ui->qBias->setValue((int) qBiasPercent); + ui->qBiasText->setText(QString(tr("%1 %").arg(qBiasPercent))); + int phaseImbalancePercent = roundf(m_settings.m_phaseImbalance * 100.0f); + ui->phaseImbalance->setValue((int) phaseImbalancePercent); + ui->phaseImbalanceText->setText(QString(tr("%1 %").arg(phaseImbalancePercent))); + ui->autoCorr->setCurrentIndex(m_settings.m_autoCorrOptions); + ui->sampleSize->blockSignals(false); + ui->modulation->setCurrentIndex((int) m_settings.m_modulation); + ui->modulationFrequency->setValue(m_settings.m_modulationTone); + ui->modulationFrequencyText->setText(QString("%1").arg(m_settings.m_modulationTone / 100.0, 0, 'f', 2)); + ui->amModulation->setValue(m_settings.m_amModulation); + ui->amModulationText->setText(QString("%1").arg(m_settings.m_amModulation)); + ui->fmDeviation->setValue(m_settings.m_fmDeviation); + ui->fmDeviationText->setText(QString("%1").arg(m_settings.m_fmDeviation / 10.0, 0, 'f', 1)); + blockApplySettings(false); +} + +void FOBOSGui::sendSettings() +{ + if (!m_updateTimer.isActive()) { + m_updateTimer.start(100); + } +} + +void FOBOSGui::updateHardware() +{ + if (m_doApplySettings) + { + FOBOSInput::MsgConfigureFOBOS* message = FOBOSInput::MsgConfigureFOBOS::create(m_settings, m_settingsKeys, m_forceSettings); + m_sampleSource->getInputMessageQueue()->push(message); + m_forceSettings = false; + m_settingsKeys.clear(); + m_updateTimer.stop(); + } +} + +void FOBOSGui::updateStatus() +{ + int state = m_deviceUISet->m_deviceAPI->state(); + + if(m_lastEngineState != state) + { + switch(state) + { + case DeviceAPI::StNotStarted: + ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + ui->power->setText(QStringLiteral("Stopped")); + break; + case DeviceAPI::StIdle: + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"); + ui->power->setText(QStringLiteral("Idle")); + break; + case DeviceAPI::StRunning: + ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + ui->power->setText(QStringLiteral("Running (%1)").arg(m_backendStatus)); + break; + case DeviceAPI::StError: + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + ui->power->setText(QStringLiteral("Error")); + QMessageBox::information(this, tr("Message"), m_deviceUISet->m_deviceAPI->errorMessage()); + break; + default: + break; + } + + m_lastEngineState = state; + } +} + +bool FOBOSGui::handleMessage(const Message& message) +{ + if (FOBOSInput::MsgConfigureFOBOS::match(message)) + { + qDebug("FOBOSGui::handleMessage: MsgConfigureFOBOS"); + const FOBOSInput::MsgConfigureFOBOS& cfg = (FOBOSInput::MsgConfigureFOBOS&) message; + + if (cfg.getForce()) { + m_settings = cfg.getSettings(); + } else { + m_settings.applySettings(cfg.getSettingsKeys(), cfg.getSettings()); + } + + displaySettings(); + return true; + } + else if (FOBOSInput::MsgReportFOBOSBackend::match(message)) + { + const FOBOSInput::MsgReportFOBOSBackend& report = (const FOBOSInput::MsgReportFOBOSBackend&) message; + m_backendStatus = report.getBackend(); + m_backendDetails = report.getDetails(); + if (m_deviceUISet->m_deviceAPI->state() == DeviceAPI::StRunning) { + ui->power->setText(QStringLiteral("Running (%1)").arg(m_backendStatus)); + } + return true; + } + else if (FOBOSInput::MsgStartStop::match(message)) + { + qDebug("FOBOSGui::handleMessage: MsgStartStop"); + FOBOSInput::MsgStartStop& notif = (FOBOSInput::MsgStartStop&) message; + blockApplySettings(true); + ui->startStop->setChecked(notif.getStartStop()); + blockApplySettings(false); + + return true; + } + else + { + return false; + } +} + +void FOBOSGui::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != 0) + { + if (DSPSignalNotification::match(*message)) + { + DSPSignalNotification* notif = (DSPSignalNotification*) message; + m_deviceSampleRate = notif->getSampleRate(); + m_deviceCenterFrequency = notif->getCenterFrequency(); + qDebug("FOBOSGui::handleInputMessages: DSPSignalNotification: SampleRate:%d, CenterFrequency:%llu", + notif->getSampleRate(), + notif->getCenterFrequency()); + updateSampleRateAndFrequency(); + + delete message; + } + else + { + if (handleMessage(*message)) + { + delete message; + } + } + } +} + +void FOBOSGui::updateSampleRateAndFrequency() +{ + m_deviceUISet->getSpectrum()->setSampleRate(m_deviceSampleRate); + m_deviceUISet->getSpectrum()->setCenterFrequency(m_deviceCenterFrequency); + ui->deviceRateText->setText(tr("%1k").arg((float)m_deviceSampleRate / 1000)); +} + +bool FOBOSGui::openFobosOperatorSettingsDialog(const QPoint& p) +{ + QDialog dialog(this); + dialog.setWindowTitle(tr("Fobos SDR settings")); + dialog.setMinimumWidth(420); + + QVBoxLayout* mainLayout = new QVBoxLayout(&dialog); + + QGroupBox* idGroup = new QGroupBox(tr("Identification"), &dialog); + QFormLayout* idLayout = new QFormLayout(idGroup); + QLineEdit* nameEdit = new QLineEdit(m_settings.m_title, idGroup); + QLabel* backendModeLabel = new QLabel(tr("Auto"), idGroup); + QLabel* detectedBackendLabel = new QLabel(m_backendStatus, idGroup); + QLabel* apiLabel = new QLabel(m_backendDetails, idGroup); + apiLabel->setWordWrap(true); + idLayout->addRow(tr("Name"), nameEdit); + idLayout->addRow(tr("Backend mode"), backendModeLabel); + idLayout->addRow(tr("Detected backend"), detectedBackendLabel); + idLayout->addRow(tr("API / board"), apiLabel); + mainLayout->addWidget(idGroup); + + QGroupBox* rxGroup = new QGroupBox(tr("Receiver"), &dialog); + QFormLayout* rxLayout = new QFormLayout(rxGroup); + + QComboBox* inputCombo = new QComboBox(rxGroup); + inputCombo->addItem(tr("RF"), (int) FOBOSSettings::InputRF); + inputCombo->addItem(tr("IQ (HF1+j*HF2) direct sampling"), (int) FOBOSSettings::InputIQDirect); + inputCombo->addItem(tr("HF1 direct sampling"), (int) FOBOSSettings::InputHF1Direct); + inputCombo->addItem(tr("HF2 direct sampling"), (int) FOBOSSettings::InputHF2Direct); + inputCombo->setCurrentIndex(qBound(0, (int) m_settings.m_inputMode, 3)); + inputCombo->setToolTip(tr("Input mode is applied on next Start. HF1/HF2 direct modes use software extraction from direct-sampling IQ.")); + + QComboBox* srCombo = new QComboBox(rxGroup); + srCombo->addItem(QStringLiteral("8"), 8000000); + srCombo->addItem(QStringLiteral("10"), 10000000); + srCombo->addItem(QStringLiteral("12.5"), 12500000); + srCombo->addItem(QStringLiteral("16"), 16000000); + srCombo->addItem(QStringLiteral("20"), 20000000); + srCombo->addItem(QStringLiteral("25"), 25000000); + srCombo->addItem(QStringLiteral("32"), 32000000); + srCombo->addItem(QStringLiteral("40"), 40000000); + srCombo->addItem(QStringLiteral("50"), 50000000); + int srIndex = srCombo->findData((int) m_settings.m_sampleRate); + if (srIndex < 0) { srIndex = srCombo->findData(25000000); } + if (srIndex < 0) { srIndex = 0; } + srCombo->setCurrentIndex(srIndex); + srCombo->setToolTip(tr("Sample Rate is changed through a controlled restart while running. Supported values are logged from the active Fobos API.")); + + QComboBox* bwCombo = new QComboBox(rxGroup); + bwCombo->addItem(tr("100%"), 100); + bwCombo->addItem(tr("90%"), 90); + bwCombo->addItem(tr("80%"), 80); + bwCombo->addItem(tr("70%"), 70); + bwCombo->addItem(tr("60%"), 60); + bwCombo->addItem(tr("50%"), 50); + bwCombo->addItem(tr("40%"), 40); + bwCombo->addItem(tr("30%"), 30); + bwCombo->addItem(tr("20%"), 20); + int bwIndex = bwCombo->findData((int) m_settings.m_bandwidthPercent); + bwCombo->setCurrentIndex(bwIndex >= 0 ? bwIndex : 1); + bwCombo->setToolTip(tr("Relative analog bandwidth. Applied by the Agile backend; shown for compatibility and ignored by the Regular backend.")); + + rxLayout->addRow(tr("Input"), inputCombo); + rxLayout->addRow(tr("Sample Rate, MSPS"), srCombo); + rxLayout->addRow(tr("Bandwidth (Agile only)"), bwCombo); + mainLayout->addWidget(rxGroup); + + QGroupBox* gainGroup = new QGroupBox(tr("Gain / display scale"), &dialog); + QFormLayout* gainLayout = new QFormLayout(gainGroup); + + // Fobos SDR: uSDR-style gain sliders. LNA has only 0..2 steps, VGA has 0..31 steps. + QWidget* lnaWidget = new QWidget(gainGroup); + QHBoxLayout* lnaRow = new QHBoxLayout(lnaWidget); + lnaRow->setContentsMargins(0, 0, 0, 0); + QSlider* lnaSlider = new QSlider(Qt::Horizontal, lnaWidget); + lnaSlider->setRange(0, 2); + lnaSlider->setSingleStep(1); + lnaSlider->setPageStep(1); + lnaSlider->setTickPosition(QSlider::TicksBelow); + lnaSlider->setTickInterval(1); + lnaSlider->setValue(qBound(0, (int) m_settings.m_autoCorrOptions, 2)); + QLabel* lnaValue = new QLabel(QStringLiteral("#%1").arg(lnaSlider->value()), lnaWidget); + lnaValue->setMinimumWidth(28); + lnaRow->addWidget(lnaSlider, 1); + lnaRow->addWidget(lnaValue); + connect(lnaSlider, &QSlider::valueChanged, lnaValue, [lnaValue](int v) { + lnaValue->setText(QStringLiteral("#%1").arg(v)); + }); + + QWidget* vgaWidget = new QWidget(gainGroup); + QHBoxLayout* vgaRow = new QHBoxLayout(vgaWidget); + vgaRow->setContentsMargins(0, 0, 0, 0); + QSlider* vgaSlider = new QSlider(Qt::Horizontal, vgaWidget); + vgaSlider->setRange(0, 31); + vgaSlider->setSingleStep(1); + vgaSlider->setPageStep(4); + vgaSlider->setTickPosition(QSlider::TicksBelow); + vgaSlider->setTickInterval(4); + vgaSlider->setValue(qBound(0, (int) m_settings.m_modulation, 31)); + QLabel* vgaValue = new QLabel(QStringLiteral("#%1").arg(vgaSlider->value()), vgaWidget); + vgaValue->setMinimumWidth(32); + vgaRow->addWidget(vgaSlider, 1); + vgaRow->addWidget(vgaValue); + connect(vgaSlider, &QSlider::valueChanged, vgaValue, [vgaValue](int v) { + vgaValue->setText(QStringLiteral("#%1").arg(v)); + }); + + QComboBox* scaleCombo = new QComboBox(gainGroup); + const QList scaleList = {1, 2, 4, 8, 16, 32, 64}; + int scaleIndex = 0; + const int currentScale = qBound(1, m_settings.m_amplitudeBits / 100, 64); + for (int i = 0; i < scaleList.size(); ++i) { + scaleCombo->addItem(QStringLiteral("x%1").arg(scaleList[i]), scaleList[i]); + if (scaleList[i] == currentScale) { + scaleIndex = i; + } + } + scaleCombo->setCurrentIndex(scaleIndex); + + gainLayout->addRow(tr("LNA Gain"), lnaWidget); + gainLayout->addRow(tr("VGA Gain"), vgaWidget); + gainLayout->addRow(tr("IQ Scale"), scaleCombo); + mainLayout->addWidget(gainGroup); + + QGroupBox* policyGroup = new QGroupBox(tr("Apply behavior"), &dialog); + QVBoxLayout* policyLayout = new QVBoxLayout(policyGroup); + QLabel* liveNote = new QLabel(tr("Live while running: Frequency in RF mode, LNA, VGA, IQ Scale, GPO, and bandwidth where supported by the active backend."), policyGroup); + QLabel* restartNote = new QLabel(tr("Controlled restart while running: Sample Rate, Input mode and External clock. SDRangel briefly stops and restarts Fobos so the spectrum scale matches real hardware settings."), policyGroup); + liveNote->setWordWrap(true); + restartNote->setWordWrap(true); + policyLayout->addWidget(liveNote); + policyLayout->addWidget(restartNote); + mainLayout->addWidget(policyGroup); + + QGroupBox* gpioGroup = new QGroupBox(tr("GPO / clock"), &dialog); + QVBoxLayout* gpioLayout = new QVBoxLayout(gpioGroup); + QHBoxLayout* gpoRow = new QHBoxLayout(); + QList gpoChecks; + for (int i = 0; i < 8; ++i) { + QCheckBox* cb = new QCheckBox(QString::number(i), gpioGroup); + cb->setChecked((m_settings.m_gpoMask & (1u << i)) != 0u); + gpoChecks.append(cb); + gpoRow->addWidget(cb); + } + QCheckBox* externalClockCheck = new QCheckBox(tr("External clock"), gpioGroup); + externalClockCheck->setChecked(m_settings.m_externalClock); + externalClockCheck->setToolTip(tr("External 10 MHz reference. Applied through controlled restart while running.")); + + // Live-apply and controlled-restart controls. OK closes with the latest values; Cancel no longer reverts + // already-applied live changes. This avoids the operator having to close the dialog for LNA/VGA/IQ/GPO/BW tweaks. + auto liveApply = [this]() { + displaySettings(); + sendSettings(); + }; + connect(lnaSlider, &QSlider::valueChanged, this, [this, liveApply](int v) { + m_settings.m_autoCorrOptions = static_cast(qBound(0, v, 2)); + m_settingsKeys.append("autoCorrOptions"); + liveApply(); + }); + connect(vgaSlider, &QSlider::valueChanged, this, [this, liveApply](int v) { + m_settings.m_modulation = static_cast(qBound(0, v, 31)); + m_settingsKeys.append("modulation"); + liveApply(); + }); + connect(scaleCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this, scaleCombo, liveApply](int) { + const int iqScale = scaleCombo->currentData().toInt(); + m_settings.m_amplitudeBits = qBound(1, iqScale, 64) * 100; + m_settingsKeys.append("amplitudeBits"); + liveApply(); + }); + connect(bwCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this, bwCombo, liveApply](int) { + m_settings.m_bandwidthPercent = bwCombo->currentData().toUInt(); + m_settingsKeys.append("bandwidthPercent"); + liveApply(); + }); + connect(inputCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this, inputCombo, liveApply](int) { + const int inputMode = inputCombo->currentData().toInt(); + m_settings.m_inputMode = static_cast(qBound(0, inputMode, 3)); + m_settingsKeys.append("inputMode"); + liveApply(); + }); + connect(srCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this, srCombo, liveApply](int) { + m_settings.m_sampleRate = srCombo->currentData().toUInt(); + m_settingsKeys.append("sampleRate"); + liveApply(); + }); + connect(externalClockCheck, &QCheckBox::toggled, this, [this, externalClockCheck, liveApply](bool) { + m_settings.m_externalClock = externalClockCheck->isChecked(); + m_settingsKeys.append("externalClock"); + liveApply(); + }); + for (int i = 0; i < gpoChecks.size(); ++i) { + connect(gpoChecks[i], &QCheckBox::toggled, this, [this, gpoChecks, liveApply](bool) { + unsigned int gpoMask = 0; + for (int bit = 0; bit < gpoChecks.size(); ++bit) { + if (gpoChecks[bit]->isChecked()) { gpoMask |= (1u << bit); } + } + m_settings.m_gpoMask = gpoMask & 0xffu; + m_settingsKeys.append("gpoMask"); + liveApply(); + }); + } + QLabel* apiNote = new QLabel(tr("Fobos SDR source. Backend mode: Auto. Agile API is tried first; regular/classic API is used as fallback. Bandwidth control is applied by Agile and shown only as an informational setting for Regular."), gpioGroup); + apiNote->setWordWrap(true); + gpioLayout->addLayout(gpoRow); + gpioLayout->addWidget(externalClockCheck); + gpioLayout->addWidget(apiNote); + mainLayout->addWidget(gpioGroup); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + mainLayout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + dialog.move(p); + new DialogPositioner(&dialog, false); + + if (dialog.exec() != QDialog::Accepted) { + return false; + } + + const QString newTitle = nameEdit->text().trimmed().isEmpty() ? QStringLiteral("Fobos SDR") : nameEdit->text().trimmed(); + m_settings.m_title = newTitle; + setTitle(m_settings.m_title); + getDeviceUISet()->m_mainSpectrumGUI->setTitle(m_settings.m_title); + m_settingsKeys.append("title"); + + const int inputMode = inputCombo->currentData().toInt(); + m_settings.m_inputMode = (FOBOSSettings::InputMode) qBound(0, inputMode, 3); + m_settingsKeys.append("inputMode"); + + m_settings.m_sampleRate = srCombo->currentData().toUInt(); + m_settingsKeys.append("sampleRate"); + + const unsigned int bw = bwCombo->currentData().toUInt(); + m_settings.m_bandwidthPercent = bw; + m_settingsKeys.append("bandwidthPercent"); + + m_settings.m_autoCorrOptions = static_cast(qBound(0, lnaSlider->value(), 2)); + m_settingsKeys.append("autoCorrOptions"); + + m_settings.m_modulation = static_cast(qBound(0, vgaSlider->value(), 31)); + m_settingsKeys.append("modulation"); + + const int iqScale = scaleCombo->currentData().toInt(); + m_settings.m_amplitudeBits = iqScale * 100; + m_settingsKeys.append("amplitudeBits"); + + unsigned int gpoMask = 0; + for (int i = 0; i < gpoChecks.size(); ++i) { + if (gpoChecks[i]->isChecked()) { + gpoMask |= (1u << i); + } + } + m_settings.m_gpoMask = gpoMask & 0xffu; + m_settingsKeys.append("gpoMask"); + + m_settings.m_externalClock = externalClockCheck->isChecked(); + m_settingsKeys.append("externalClock"); + + displaySettings(); + sendSettings(); + return true; +} + +void FOBOSGui::openDeviceSettingsDialog(const QPoint& p) +{ + if (m_contextMenuType == ContextMenuDeviceSettings) + { + // Fobos SDR: uSDR-style operator settings dialog with controlled restart for critical settings. + openFobosOperatorSettingsDialog(p); + } + + resetContextMenuType(); +} + +void FOBOSGui::makeUIConnections() +{ + QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &FOBOSGui::on_startStop_toggled); + QObject::connect(ui->centerFrequency, &ValueDial::changed, this, &FOBOSGui::on_centerFrequency_changed); + QObject::connect(ui->autoCorr, QOverload::of(&QComboBox::currentIndexChanged), this, &FOBOSGui::on_autoCorr_currentIndexChanged); + QObject::connect(ui->frequencyShift, &ValueDialZ::changed, this, &FOBOSGui::on_frequencyShift_changed); + QObject::connect(ui->decimation, QOverload::of(&QComboBox::currentIndexChanged), this, &FOBOSGui::on_decimation_currentIndexChanged); + QObject::connect(ui->fcPos, QOverload::of(&QComboBox::currentIndexChanged), this, &FOBOSGui::on_fcPos_currentIndexChanged); + QObject::connect(ui->sampleRate, &ValueDial::changed, this, &FOBOSGui::on_sampleRate_changed); + QObject::connect(ui->sampleSize, QOverload::of(&QComboBox::currentIndexChanged), this, &FOBOSGui::on_sampleSize_currentIndexChanged); + QObject::connect(ui->amplitudeCoarse, &QSlider::valueChanged, this, &FOBOSGui::on_amplitudeCoarse_valueChanged); + QObject::connect(ui->amplitudeFine, &QSlider::valueChanged, this, &FOBOSGui::on_amplitudeFine_valueChanged); + QObject::connect(ui->modulation, QOverload::of(&QComboBox::currentIndexChanged), this, &FOBOSGui::on_modulation_currentIndexChanged); + QObject::connect(ui->modulationFrequency, &QDial::valueChanged, this, &FOBOSGui::on_modulationFrequency_valueChanged); + QObject::connect(ui->amModulation, &QDial::valueChanged, this, &FOBOSGui::on_amModulation_valueChanged); + QObject::connect(ui->fmDeviation, &QDial::valueChanged, this, &FOBOSGui::on_fmDeviation_valueChanged); + QObject::connect(ui->dcBias, &QSlider::valueChanged, this, &FOBOSGui::on_dcBias_valueChanged); + QObject::connect(ui->iBias, &QSlider::valueChanged, this, &FOBOSGui::on_iBias_valueChanged); + QObject::connect(ui->qBias, &QSlider::valueChanged, this, &FOBOSGui::on_qBias_valueChanged); + QObject::connect(ui->phaseImbalance, &QSlider::valueChanged, this, &FOBOSGui::on_phaseImbalance_valueChanged); +} diff --git a/plugins/samplesource/fobos/fobosgui.h b/plugins/samplesource/fobos/fobosgui.h new file mode 100644 index 000000000..21c2cea13 --- /dev/null +++ b/plugins/samplesource/fobos/fobosgui.h @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2022 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 _FOBOS_FOBOSGUI_H_ +#define _FOBOS_FOBOSGUI_H_ + +#include +#include +#include +#include + +#include "util/messagequeue.h" + +#include "FOBOSsettings.h" +#include "FOBOSinput.h" + +class DeviceUISet; + +namespace Ui { + class FOBOSGui; +} + +class FOBOSGui : public DeviceGUI { + Q_OBJECT + +public: + explicit FOBOSGui(DeviceUISet *deviceUISet, QWidget* parent = 0); + virtual ~FOBOSGui(); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + Ui::FOBOSGui* ui; + + FOBOSSettings m_settings; + QList m_settingsKeys; + QTimer m_updateTimer; + QTimer m_statusTimer; + bool m_doApplySettings; + bool m_forceSettings; + DeviceSampleSource* m_sampleSource; + std::size_t m_tickCount; + int m_deviceSampleRate; + quint64 m_deviceCenterFrequency; //!< Center frequency in device + int m_lastEngineState; + MessageQueue m_inputMessageQueue; + + void blockApplySettings(bool block) { m_doApplySettings = !block; } + void displaySettings(); + void sendSettings(); + void updateSampleRateAndFrequency(); + void displayAmplitude(); + void updateAmpCoarseLimit(); + void updateAmpFineLimit(); + void updateFrequencyShiftLimit(); + bool handleMessage(const Message& message); + void makeUIConnections(); + bool openFobosOperatorSettingsDialog(const QPoint& p); + +private slots: + void handleInputMessages(); + void on_startStop_toggled(bool checked); + void on_centerFrequency_changed(quint64 value); + void on_autoCorr_currentIndexChanged(int index); + void on_frequencyShift_changed(qint64 value); + void on_decimation_currentIndexChanged(int index); + void on_fcPos_currentIndexChanged(int index); + void on_sampleRate_changed(quint64 value); + void on_sampleSize_currentIndexChanged(int index); + void on_amplitudeCoarse_valueChanged(int value); + void on_amplitudeFine_valueChanged(int value); + void on_modulation_currentIndexChanged(int index); + void on_modulationFrequency_valueChanged(int value); + void on_amModulation_valueChanged(int value); + void on_fmDeviation_valueChanged(int value); + void on_dcBias_valueChanged(int value); + void on_iBias_valueChanged(int value); + void on_qBias_valueChanged(int value); + void on_phaseImbalance_valueChanged(int value); + void openDeviceSettingsDialog(const QPoint& p); + void updateStatus(); + void updateHardware(); +private: + QString m_backendStatus; + QString m_backendDetails; + +}; + +#endif // _FOBOS_FOBOSGUI_H_ diff --git a/plugins/samplesource/fobos/fobosgui.ui b/plugins/samplesource/fobos/fobosgui.ui new file mode 100644 index 000000000..a759449f1 --- /dev/null +++ b/plugins/samplesource/fobos/fobosgui.ui @@ -0,0 +1,1041 @@ + + + FOBOSGui + + + + 0 + 0 + 360 + 331 + + + + + 0 + 0 + + + + + 360 + 331 + + + + + 380 + 343 + + + + + Liberation Sans + 9 + false + false + + + + Fobos SDR + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 4 + + + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + + + + + + 58 + 0 + + + + I/Q sample rate kS/s + + + 0000.00k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 16 + false + false + + + + PointingHandCursor + + + Qt::StrongFocus + + + Tuner center frequency in kHz + + + + + + + kHz + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Corr + + + + + + + DC offset and IQ correction options + + + + None + + + + + DC + + + + + DC+IQ + + + + + + + + Dec + + + + + + + + 45 + 16777215 + + + + Decimation factor + + + + 1 + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + 32 + + + + + 64 + + + + + + + + Fp + + + + + + + + 55 + 0 + + + + + 50 + 16777215 + + + + Relative position of generator center frequency + + + 2 + + + + Inf + + + + + Sup + + + + + Cen + + + + + + + + Sz + + + + + + + + 45 + 16777215 + + + + Sample size + + + 0 + + + + 8 + + + + + 12 + + + + + 16 + + + + + + + + bits + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 2 + + + 2 + + + + + + 0 + 0 + + + + + 16 + 0 + + + + SR + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + false + false + + + + PointingHandCursor + + + Generator sample rate (S/s) + + + + + + + S/s + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Mod + + + + + + + + 50 + 16777215 + + + + Modulation + + + + No + + + + + AM + + + + + FM + + + + + P0 + + + + + P1 + + + + + P2 + + + + + + + + + 22 + 22 + + + + Modulation tone (kHz) + + + 1 + + + 999 + + + 1 + + + + + + + + 35 + 0 + + + + Modulation tone value (kHz) + + + 0.00 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + 16 + 0 + + + + D + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + false + false + + + + PointingHandCursor + + + Shift from center frequency + + + + + + + Hz + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + AM + + + + + + + + 22 + 22 + + + + AM modulation (%) + + + 1 + + + + + + + AM modulation value (%) + + + 00 + + + + + + + Qt::Vertical + + + + + + + FM + + + + + + + + 22 + 22 + + + + FM deviation (kHz) + + + 1 + + + 999 + + + 1 + + + + + + + + 35 + 0 + + + + FM deviation value (kHz) + + + 00.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + + 0 + 22 + + + + Amp fine + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Amp coarse + + + + + + + true + + + Amplitude coarse (x100) + + + 327 + + + 1 + + + Qt::Horizontal + + + + + + + Amplitude in bits + + + 32768 b + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 52 + 0 + + + + Power + + + -100 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Amplitude fine (x1) + + + 1 + + + Qt::Horizontal + + + + + + + + + + + -99 + + + 1 + + + Qt::Horizontal + + + + + + + -100 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 22 + + + + Q bias + + + + + + + + + + + + + + + - + + + + + + + + 0 + 22 + + + + I bias + + + + + + + - + + + + + + + -99 + + + 1 + + + Qt::Horizontal + + + + + + + -100 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + 0 + 22 + + + + DC bias + + + + + + + - + + + + + + + -99 + + + 1 + + + Qt::Horizontal + + + + + + + + + + + + + + + -100 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 22 + + + + Phase + + + + + + + - + + + + + + + + + + + + + + + + 45 + 0 + + + + -100 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + -99 + + + 1 + + + Qt::Horizontal + + + + + + + + + + ValueDial + QWidget +
gui/valuedial.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+
+ + + + +
diff --git a/plugins/samplesource/fobos/fobosinput.cpp b/plugins/samplesource/fobos/fobosinput.cpp new file mode 100644 index 000000000..b7cc7639a --- /dev/null +++ b/plugins/samplesource/fobos/fobosinput.cpp @@ -0,0 +1,875 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018-2020, 2022-2023 Edouard Griffiths, F4EXB // +// Copyright (C) 2018 beta-tester // +// // +// 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 + +#include "SWGDeviceSettings.h" +#include "SWGDeviceState.h" + +#include "FOBOSinput.h" +#include "device/deviceapi.h" +#include "FOBOSworker.h" +#include "dsp/dspcommands.h" + +MESSAGE_CLASS_DEFINITION(FOBOSInput::MsgConfigureFOBOS, Message) +MESSAGE_CLASS_DEFINITION(FOBOSInput::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(FOBOSInput::MsgReportFOBOSBackend, Message) + +namespace +{ + static const char* kFobosInputLogPath = "sdrangel_fobos_source.log"; + + static void fobosInputLog(const char* fmt, ...) + { + FILE* f = std::fopen(kFobosInputLogPath, "a"); + if (!f) { + return; + } + va_list ap; + va_start(ap, fmt); + std::vfprintf(f, fmt, ap); + va_end(ap); + std::fprintf(f, "\n"); + std::fclose(f); + } + + static const char* boolText(bool v) + { + return v ? "true" : "false"; + } +} + + +FOBOSInput::FOBOSInput(DeviceAPI *deviceAPI) : + m_deviceAPI(deviceAPI), + m_settings(), + m_worker(nullptr), + m_workerThread(nullptr), + m_deviceDescription("FOBOSInput"), + m_running(false), + m_masterTimer(deviceAPI->getMasterTimer()) +{ + m_sampleFifo.setLabel(m_deviceDescription); + m_deviceAPI->setNbSourceStreams(1); + + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &FOBOSInput::networkManagerFinished + ); +} + +FOBOSInput::~FOBOSInput() +{ + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &FOBOSInput::networkManagerFinished + ); + delete m_networkManager; + + if (m_running) { + stop(); + } +} + +void FOBOSInput::destroy() +{ + delete this; +} + +void FOBOSInput::init() +{ + applySettings(m_settings, QList(), true); +} + +bool FOBOSInput::start() +{ + QMutexLocker mutexLocker(&m_mutex); + + if (m_running) { + return true; + } + + if (!m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(m_settings.m_sampleRate))) + { + qCritical("FOBOSInput::FOBOSInput: Could not allocate SampleFifo"); + return false; + } + + m_workerThread = new QThread(); + m_worker = new FOBOSWorker(&m_sampleFifo); + m_worker->moveToThread(m_workerThread); + + QObject::connect(m_workerThread, &QThread::started, m_worker, &FOBOSWorker::startWork); + QObject::connect(m_worker, &FOBOSWorker::backendStatusChanged, this, &FOBOSInput::handleWorkerBackendStatus, Qt::QueuedConnection); + QObject::connect(m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater, Qt::QueuedConnection); + QObject::connect(m_workerThread, &QThread::finished, m_workerThread, &QThread::deleteLater); + + m_worker->setSamplerate(m_settings.m_sampleRate); + // FOBOS_START_SETTINGS_HANDOFF + m_worker->setCenterFrequency(m_settings.m_centerFrequency); + m_worker->setLog2Decimation(m_settings.m_log2Decim); + m_worker->setFcPos(static_cast(m_settings.m_fcPos)); + m_worker->setFrequencyShift(m_settings.m_frequencyShift); + m_worker->setAmplitudeBits(m_settings.m_amplitudeBits); + m_worker->setLnaGain(static_cast(m_settings.m_autoCorrOptions)); + m_worker->setModulation(static_cast(m_settings.m_modulation)); + m_worker->setInputMode(static_cast(m_settings.m_inputMode)); + m_worker->setBandwidthPercent(m_settings.m_bandwidthPercent); + m_worker->setGpoMask(m_settings.m_gpoMask); + m_worker->setExternalClock(m_settings.m_externalClock); + m_workerThread->start(); + m_running = true; + + mutexLocker.unlock(); + + applySettings(m_settings, QList(), true); + + return true; +} + +void FOBOSInput::stop() +{ + QMutexLocker mutexLocker(&m_mutex); + + if (!m_running) { + return; + } + + m_running = false; + + if (m_workerThread) + { + m_worker->stopWork(); + m_workerThread->quit(); + m_workerThread->wait(); + m_worker = nullptr; + m_workerThread = nullptr; + } +} + +QByteArray FOBOSInput::serialize() const +{ + return m_settings.serialize(); +} + +bool FOBOSInput::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureFOBOS* message = MsgConfigureFOBOS::create(m_settings, QList(), true); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) + { + MsgConfigureFOBOS* messageToGUI = MsgConfigureFOBOS::create(m_settings, QList(), true); + m_guiMessageQueue->push(messageToGUI); + } + + return success; +} + +const QString& FOBOSInput::getDeviceDescription() const +{ + return m_deviceDescription; +} + +int FOBOSInput::getSampleRate() const +{ + return m_settings.m_sampleRate/(1<{"centerFrequency"}, false); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) + { + MsgConfigureFOBOS* messageToGUI = MsgConfigureFOBOS::create(settings, QList{"centerFrequency"}, false); + m_guiMessageQueue->push(messageToGUI); + } +} + +bool FOBOSInput::handleMessage(const Message& message) +{ + if (MsgConfigureFOBOS::match(message)) + { + MsgConfigureFOBOS& conf = (MsgConfigureFOBOS&) message; + qDebug() << "FOBOSInput::handleMessage: MsgConfigureFOBOS"; + + bool success = applySettings(conf.getSettings(), conf.getSettingsKeys(), conf.getForce()); + + if (!success) + { + qDebug("FOBOSInput::handleMessage: config error"); + } + + return true; + } + else if (MsgStartStop::match(message)) + { + MsgStartStop& cmd = (MsgStartStop&) message; + qDebug() << "FOBOSInput::handleMessage: MsgStartStop: " << (cmd.getStartStop() ? "start" : "stop"); + + if (cmd.getStartStop()) + { + if (m_deviceAPI->initDeviceEngine()) + { + m_deviceAPI->startDeviceEngine(); + } + } + else + { + m_deviceAPI->stopDeviceEngine(); + } + + if (m_settings.m_useReverseAPI) { + webapiReverseSendStartStop(cmd.getStartStop()); + } + + return true; + } + else if (MsgReportFOBOSBackend::match(message)) + { + const MsgReportFOBOSBackend& report = (const MsgReportFOBOSBackend&) message; + if (m_guiMessageQueue) { + MsgReportFOBOSBackend* msgToGUI = MsgReportFOBOSBackend::create(report.getBackend(), report.getDetails()); + m_guiMessageQueue->push(msgToGUI); + } + return true; + } + else + { + return false; + } +} + + +void FOBOSInput::handleWorkerBackendStatus(const QString& backend, const QString& details) +{ + if (m_guiMessageQueue) { + MsgReportFOBOSBackend* msgToGUI = MsgReportFOBOSBackend::create(backend, details); + m_guiMessageQueue->push(msgToGUI); + } +} + +bool FOBOSInput::applySettings(const FOBOSSettings& settings, const QList& settingsKeys, bool force) +{ + qDebug() << "FOBOSInput::applySettings: force:" << force << settings.getDebugString(settingsKeys, force); + + const bool criticalRestartRequired = m_running && !force && + (settingsKeys.contains("sampleRate") || + settingsKeys.contains("inputMode") || + settingsKeys.contains("externalClock")); + + if (criticalRestartRequired) + { + QStringList keyStrings; + for (const QString& key : settingsKeys) { + keyStrings.append(key); + } + const QByteArray keyBytes = keyStrings.join(QStringLiteral(",")).toUtf8(); + + fobosInputLog("controlled_restart=START reason=critical_settings_changed keys='%s' old_sr=%u new_sr=%u old_input=%d new_input=%d old_external_clock=%s new_external_clock=%s", + keyBytes.constData(), + static_cast(m_settings.m_sampleRate), + static_cast(settings.m_sampleRate), + static_cast(m_settings.m_inputMode), + static_cast(settings.m_inputMode), + boolText(m_settings.m_externalClock), + boolText(settings.m_externalClock)); + + FOBOSSettings restartSettings = m_settings; + restartSettings.applySettings(settingsKeys, settings); + m_settings = restartSettings; + + const int sampleRate = m_settings.m_sampleRate/(1<getDeviceEngineInputMessageQueue()->push(notif); + + stop(); + fobosInputLog("controlled_restart=AFTER_STOP"); + + const bool restartOk = start(); + fobosInputLog("controlled_restart=RETURN result=%s new_sr=%u new_input=%d new_external_clock=%s", + restartOk ? "OK" : "FAILED", + static_cast(m_settings.m_sampleRate), + static_cast(m_settings.m_inputMode), + boolText(m_settings.m_externalClock)); + return restartOk; + } + + if (settingsKeys.contains("autoCorrOptions") || force) + { + switch(settings.m_autoCorrOptions) + { + case FOBOSSettings::AutoCorrDC: + m_deviceAPI->configureCorrections(true, false); + break; + case FOBOSSettings::AutoCorrDCAndIQ: + m_deviceAPI->configureCorrections(true, true); + break; + case FOBOSSettings::AutoCorrNone: + default: + m_deviceAPI->configureCorrections(false, false); + break; + } + + if (m_worker != 0) { + m_worker->setLnaGain(static_cast(settings.m_autoCorrOptions)); + } + } + + if (settingsKeys.contains("sampleRate") || force) + { + if (m_worker != 0) + { + m_worker->setSamplerate(settings.m_sampleRate); + qDebug("FOBOSInput::applySettings: sample rate set to %d", settings.m_sampleRate); + } + } + + if (settingsKeys.contains("log2Decim") || force) + { + if (m_worker != 0) + { + m_worker->setLog2Decimation(settings.m_log2Decim); + qDebug() << "FOBOSInput::applySettings: set decimation to " << (1<setFcPos((int) settings.m_fcPos); + m_worker->setFrequencyShift(frequencyShift); + // FOBOS_APPLY_CENTER_HANDOFF + m_worker->setCenterFrequency(deviceCenterFrequency); + qDebug() << "FOBOSInput::applySettings:" + << " center freq: " << settings.m_centerFrequency << " Hz" + << " device center freq: " << deviceCenterFrequency << " Hz" + << " device sample rate: " << devSampleRate << "Hz" + << " Actual sample rate: " << devSampleRate/(1<setAmplitudeBits(settings.m_amplitudeBits); + } + } + + if (settingsKeys.contains("dcFactor") || force) + { + if (m_worker != 0) { + m_worker->setDCFactor(settings.m_dcFactor); + } + } + + if (settingsKeys.contains("iFactor") || force) + { + if (m_worker != 0) { + m_worker->setIFactor(settings.m_iFactor); + } + } + + if (settingsKeys.contains("qFactor") || force) + { + if (m_worker != 0) { + m_worker->setQFactor(settings.m_qFactor); + } + } + + if (settingsKeys.contains("phaseImbalance") || force) + { + if (m_worker != 0) { + m_worker->setPhaseImbalance(settings.m_phaseImbalance); + } + } + + if (settingsKeys.contains("sampleSizeIndex") || force) + { + if (m_worker != 0) { + m_worker->setBitSize(settings.m_sampleSizeIndex); + } + } + + // if ((m_settings.m_sampleRate != settings.m_sampleRate) + // || (m_settings.m_centerFrequency != settings.m_centerFrequency) + // || (m_settings.m_log2Decim != settings.m_log2Decim) + // || (m_settings.m_fcPos != settings.m_fcPos) || force) + if (settingsKeys.contains("sampleRate") + || settingsKeys.contains("centerFrequency") + || settingsKeys.contains("log2Decim") + || settingsKeys.contains("fcPos") || force) + { + int sampleRate = settings.m_sampleRate/(1<getDeviceEngineInputMessageQueue()->push(notif); + } + + if (settingsKeys.contains("inputMode") || force) + { + if (m_worker != 0) { + m_worker->setInputMode(static_cast(settings.m_inputMode)); + } + } + + if (settingsKeys.contains("bandwidthPercent") || force) + { + if (m_worker != 0) { + m_worker->setBandwidthPercent(settings.m_bandwidthPercent); + } + } + + if (settingsKeys.contains("gpoMask") || force) + { + if (m_worker != 0) { + m_worker->setGpoMask(settings.m_gpoMask); + } + } + + if (settingsKeys.contains("externalClock") || force) + { + if (m_worker != 0) { + m_worker->setExternalClock(settings.m_externalClock); + } + } + + if (settingsKeys.contains("modulationTone") || force) + { + if (m_worker != 0) { + m_worker->setToneFrequency(settings.m_modulationTone * 10); + } + } + + if (settingsKeys.contains("modulation") || force) + { + if (m_worker != 0) + { + m_worker->setModulation(settings.m_modulation); + + if (settings.m_modulation == FOBOSSettings::ModulationPattern0) { + m_worker->setPattern0(); + } else if (settings.m_modulation == FOBOSSettings::ModulationPattern1) { + m_worker->setPattern1(); + } else if (settings.m_modulation == FOBOSSettings::ModulationPattern2) { + m_worker->setPattern2(); + } + } + } + + if (settingsKeys.contains("amModulation") || force) + { + if (m_worker != 0) { + m_worker->setAMModulation(settings.m_amModulation / 100.0f); + } + } + + if (settingsKeys.contains("fmDeviation") || force) + { + if (m_worker != 0) { + m_worker->setFMDeviation(settings.m_fmDeviation * 100.0f); + } + } + + if (settings.m_useReverseAPI) + { + bool fullUpdate = (settingsKeys.contains("useReverseAPI") && settings.m_useReverseAPI) || + settingsKeys.contains("reverseAPIAddress") || + settingsKeys.contains("reverseAPIPort") || + settingsKeys.contains("reverseAPIDeviceIndex"); + webapiReverseSendSettings(settingsKeys, settings, fullUpdate || force); + } + + if (force) { + m_settings = settings; + } else { + m_settings.applySettings(settingsKeys, settings); + } + + return true; +} + +int FOBOSInput::webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + return 200; +} + +int FOBOSInput::webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + MsgStartStop *message = MsgStartStop::create(run); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgStartStop *msgToGUI = MsgStartStop::create(run); + m_guiMessageQueue->push(msgToGUI); + } + + return 200; +} + +int FOBOSInput::webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setTestSourceSettings(new SWGSDRangel::SWGTestSourceSettings()); + response.getTestSourceSettings()->init(); + webapiFormatDeviceSettings(response, m_settings); + return 200; +} + +int FOBOSInput::webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage) +{ + (void) errorMessage; + FOBOSSettings settings = m_settings; + webapiUpdateDeviceSettings(settings, deviceSettingsKeys, response); + + MsgConfigureFOBOS *msg = MsgConfigureFOBOS::create(settings, deviceSettingsKeys, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureFOBOS *msgToGUI = MsgConfigureFOBOS::create(settings, deviceSettingsKeys, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatDeviceSettings(response, settings); + return 200; +} + +void FOBOSInput::webapiUpdateDeviceSettings( + FOBOSSettings& settings, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response) +{ + if (deviceSettingsKeys.contains("title")) { + settings.m_title = *response.getTestSourceSettings()->getTitle(); + } + if (deviceSettingsKeys.contains("centerFrequency")) { + settings.m_centerFrequency = response.getTestSourceSettings()->getCenterFrequency(); + } + if (deviceSettingsKeys.contains("frequencyShift")) { + settings.m_frequencyShift = response.getTestSourceSettings()->getFrequencyShift(); + } + if (deviceSettingsKeys.contains("sampleRate")) { + settings.m_sampleRate = response.getTestSourceSettings()->getSampleRate(); + } + if (deviceSettingsKeys.contains("log2Decim")) { + settings.m_log2Decim = response.getTestSourceSettings()->getLog2Decim(); + } + if (deviceSettingsKeys.contains("fcPos")) { + int fcPos = response.getTestSourceSettings()->getFcPos(); + fcPos = fcPos < 0 ? 0 : fcPos > 2 ? 2 : fcPos; + settings.m_fcPos = (FOBOSSettings::fcPos_t) fcPos; + } + if (deviceSettingsKeys.contains("sampleSizeIndex")) { + int sampleSizeIndex = response.getTestSourceSettings()->getSampleSizeIndex(); + sampleSizeIndex = sampleSizeIndex < 0 ? 0 : sampleSizeIndex > 1 ? 2 : sampleSizeIndex; + settings.m_sampleSizeIndex = sampleSizeIndex; + } + if (deviceSettingsKeys.contains("amplitudeBits")) { + settings.m_amplitudeBits = response.getTestSourceSettings()->getAmplitudeBits(); + } + if (deviceSettingsKeys.contains("autoCorrOptions")) { + int autoCorrOptions = response.getTestSourceSettings()->getAutoCorrOptions(); + autoCorrOptions = autoCorrOptions < 0 ? 0 : autoCorrOptions >= FOBOSSettings::AutoCorrLast ? FOBOSSettings::AutoCorrLast-1 : autoCorrOptions; + settings.m_sampleSizeIndex = (FOBOSSettings::AutoCorrOptions) autoCorrOptions; + } + if (deviceSettingsKeys.contains("modulation")) { + int modulation = response.getTestSourceSettings()->getModulation(); + modulation = modulation < 0 ? 0 : modulation >= FOBOSSettings::ModulationLast ? FOBOSSettings::ModulationLast-1 : modulation; + settings.m_modulation = (FOBOSSettings::Modulation) modulation; + } + if (deviceSettingsKeys.contains("modulationTone")) { + settings.m_modulationTone = response.getTestSourceSettings()->getModulationTone(); + } + if (deviceSettingsKeys.contains("amModulation")) { + settings.m_amModulation = response.getTestSourceSettings()->getAmModulation(); + }; + if (deviceSettingsKeys.contains("fmDeviation")) { + settings.m_fmDeviation = response.getTestSourceSettings()->getFmDeviation(); + }; + if (deviceSettingsKeys.contains("dcFactor")) { + settings.m_dcFactor = response.getTestSourceSettings()->getDcFactor(); + }; + if (deviceSettingsKeys.contains("iFactor")) { + settings.m_iFactor = response.getTestSourceSettings()->getIFactor(); + }; + if (deviceSettingsKeys.contains("qFactor")) { + settings.m_qFactor = response.getTestSourceSettings()->getQFactor(); + }; + if (deviceSettingsKeys.contains("phaseImbalance")) { + settings.m_phaseImbalance = response.getTestSourceSettings()->getPhaseImbalance(); + }; + if (deviceSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getTestSourceSettings()->getUseReverseApi() != 0; + } + if (deviceSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getTestSourceSettings()->getReverseApiAddress(); + } + if (deviceSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getTestSourceSettings()->getReverseApiPort(); + } + if (deviceSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getTestSourceSettings()->getReverseApiDeviceIndex(); + } +} + +void FOBOSInput::webapiFormatDeviceSettings(SWGSDRangel::SWGDeviceSettings& response, const FOBOSSettings& settings) +{ + if (response.getTestSourceSettings()->getTitle()) { + *response.getTestSourceSettings()->getTitle() = settings.m_title; + } else { + response.getTestSourceSettings()->setTitle(new QString(settings.m_title)); + } + + response.getTestSourceSettings()->setCenterFrequency(settings.m_centerFrequency); + response.getTestSourceSettings()->setFrequencyShift(settings.m_frequencyShift); + response.getTestSourceSettings()->setSampleRate(settings.m_sampleRate); + response.getTestSourceSettings()->setLog2Decim(settings.m_log2Decim); + response.getTestSourceSettings()->setFcPos((int) settings.m_fcPos); + response.getTestSourceSettings()->setSampleSizeIndex((int) settings.m_sampleSizeIndex); + response.getTestSourceSettings()->setAmplitudeBits(settings.m_amplitudeBits); + response.getTestSourceSettings()->setAutoCorrOptions((int) settings.m_autoCorrOptions); + response.getTestSourceSettings()->setModulation((int) settings.m_modulation); + response.getTestSourceSettings()->setModulationTone(settings.m_modulationTone); + response.getTestSourceSettings()->setAmModulation(settings.m_amModulation); + response.getTestSourceSettings()->setFmDeviation(settings.m_fmDeviation); + response.getTestSourceSettings()->setDcFactor(settings.m_dcFactor); + response.getTestSourceSettings()->setIFactor(settings.m_iFactor); + response.getTestSourceSettings()->setQFactor(settings.m_qFactor); + response.getTestSourceSettings()->setPhaseImbalance(settings.m_phaseImbalance); + + response.getTestSourceSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getTestSourceSettings()->getReverseApiAddress()) { + *response.getTestSourceSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getTestSourceSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getTestSourceSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getTestSourceSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); +} + +void FOBOSInput::webapiReverseSendSettings(const QList& deviceSettingsKeys, const FOBOSSettings& settings, bool force) +{ + SWGSDRangel::SWGDeviceSettings *swgDeviceSettings = new SWGSDRangel::SWGDeviceSettings(); + swgDeviceSettings->setDirection(0); // single Rx + swgDeviceSettings->setOriginatorIndex(m_deviceAPI->getDeviceSetIndex()); + swgDeviceSettings->setDeviceHwType(new QString("FOBOS")); + swgDeviceSettings->setTestSourceSettings(new SWGSDRangel::SWGTestSourceSettings()); + SWGSDRangel::SWGTestSourceSettings *SWGTestSourceSettings = swgDeviceSettings->getTestSourceSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (deviceSettingsKeys.contains("title") || force) { + SWGTestSourceSettings->setTitle(new QString(settings.m_title)); + } + if (deviceSettingsKeys.contains("centerFrequency") || force) { + SWGTestSourceSettings->setCenterFrequency(settings.m_centerFrequency); + } + if (deviceSettingsKeys.contains("frequencyShift") || force) { + SWGTestSourceSettings->setFrequencyShift(settings.m_frequencyShift); + } + if (deviceSettingsKeys.contains("sampleRate") || force) { + SWGTestSourceSettings->setSampleRate(settings.m_sampleRate); + } + if (deviceSettingsKeys.contains("log2Decim") || force) { + SWGTestSourceSettings->setLog2Decim(settings.m_log2Decim); + } + if (deviceSettingsKeys.contains("fcPos") || force) { + SWGTestSourceSettings->setFcPos((int) settings.m_fcPos); + } + if (deviceSettingsKeys.contains("sampleSizeIndex") || force) { + SWGTestSourceSettings->setSampleSizeIndex(settings.m_sampleSizeIndex); + } + if (deviceSettingsKeys.contains("amplitudeBits") || force) { + SWGTestSourceSettings->setAmplitudeBits(settings.m_amplitudeBits); + } + if (deviceSettingsKeys.contains("autoCorrOptions") || force) { + SWGTestSourceSettings->setAutoCorrOptions((int) settings.m_sampleSizeIndex); + } + if (deviceSettingsKeys.contains("modulation") || force) { + SWGTestSourceSettings->setModulation((int) settings.m_modulation); + } + if (deviceSettingsKeys.contains("modulationTone")) { + SWGTestSourceSettings->setModulationTone(settings.m_modulationTone); + } + if (deviceSettingsKeys.contains("amModulation") || force) { + SWGTestSourceSettings->setAmModulation(settings.m_amModulation); + }; + if (deviceSettingsKeys.contains("fmDeviation") || force) { + SWGTestSourceSettings->setFmDeviation(settings.m_fmDeviation); + }; + if (deviceSettingsKeys.contains("dcFactor") || force) { + SWGTestSourceSettings->setDcFactor(settings.m_dcFactor); + }; + if (deviceSettingsKeys.contains("iFactor") || force) { + SWGTestSourceSettings->setIFactor(settings.m_iFactor); + }; + if (deviceSettingsKeys.contains("qFactor") || force) { + SWGTestSourceSettings->setQFactor(settings.m_qFactor); + }; + if (deviceSettingsKeys.contains("phaseImbalance") || force) { + SWGTestSourceSettings->setPhaseImbalance(settings.m_phaseImbalance); + }; + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/device/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgDeviceSettings->asJson().toUtf8()); + buffer->seek(0); +// qDebug("FOBOSInput::webapiReverseSendSettings: %s", channelSettingsURL.toStdString().c_str()); +// qDebug("FOBOSInput::webapiReverseSendSettings: query:\n%s", swgDeviceSettings->asJson().toStdString().c_str()); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgDeviceSettings; +} + +void FOBOSInput::webapiReverseSendStartStop(bool start) +{ + SWGSDRangel::SWGDeviceSettings *swgDeviceSettings = new SWGSDRangel::SWGDeviceSettings(); + swgDeviceSettings->setDirection(0); // single Rx + swgDeviceSettings->setOriginatorIndex(m_deviceAPI->getDeviceSetIndex()); + swgDeviceSettings->setDeviceHwType(new QString("FOBOS")); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/device/run") + .arg(m_settings.m_reverseAPIAddress) + .arg(m_settings.m_reverseAPIPort) + .arg(m_settings.m_reverseAPIDeviceIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgDeviceSettings->asJson().toUtf8()); + buffer->seek(0); + QNetworkReply *reply; + + if (start) { + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + } else { + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + } + + buffer->setParent(reply); + delete swgDeviceSettings; +} + +void FOBOSInput::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "FOBOSInput::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("FOBOSInput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/samplesource/fobos/fobosinput.h b/plugins/samplesource/fobos/fobosinput.h new file mode 100644 index 000000000..dab176a38 --- /dev/null +++ b/plugins/samplesource/fobos/fobosinput.h @@ -0,0 +1,176 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2014 John Greb // +// Copyright (C) 2015-2020, 2022 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 _FOBOS_FOBOSINPUT_H_ +#define _FOBOS_FOBOSINPUT_H_ + +#include +#include +#include +#include + +#include +#include "FOBOSsettings.h" + +class DeviceAPI; +class FOBOSWorker; +class QNetworkAccessManager; +class QNetworkReply; +class QThread; + +class FOBOSInput : public DeviceSampleSource { + Q_OBJECT +public: + class MsgConfigureFOBOS : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const FOBOSSettings& getSettings() const { return m_settings; } + const QList& getSettingsKeys() const { return m_settingsKeys; } + bool getForce() const { return m_force; } + + static MsgConfigureFOBOS* create(const FOBOSSettings& settings, const QList& settingsKeys, bool force) { + return new MsgConfigureFOBOS(settings, settingsKeys, force); + } + + private: + FOBOSSettings m_settings; + QList m_settingsKeys; + bool m_force; + + MsgConfigureFOBOS(const FOBOSSettings& settings, const QList& settingsKeys, bool force) : + Message(), + m_settings(settings), + m_settingsKeys(settingsKeys), + m_force(force) + { } + }; + + class MsgStartStop : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgStartStop* create(bool startStop) { + return new MsgStartStop(startStop); + } + + protected: + bool m_startStop; + + MsgStartStop(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + class MsgReportFOBOSBackend : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getBackend() const { return m_backend; } + const QString& getDetails() const { return m_details; } + + static MsgReportFOBOSBackend* create(const QString& backend, const QString& details) { + return new MsgReportFOBOSBackend(backend, details); + } + + protected: + QString m_backend; + QString m_details; + + MsgReportFOBOSBackend(const QString& backend, const QString& details) : + Message(), + m_backend(backend), + m_details(details) + { } + }; + + FOBOSInput(DeviceAPI *deviceAPI); + virtual ~FOBOSInput(); + virtual void destroy(); + + virtual void init(); + virtual bool start(); + virtual void stop(); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual void setMessageQueueToGUI(MessageQueue *queue) { m_guiMessageQueue = queue; } + virtual const QString& getDeviceDescription() const; + virtual int getSampleRate() const; + virtual void setSampleRate(int sampleRate) { (void) sampleRate; } + virtual quint64 getCenterFrequency() const; + virtual void setCenterFrequency(qint64 centerFrequency); + + virtual bool handleMessage(const Message& message); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage); + + virtual int webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + static void webapiFormatDeviceSettings( + SWGSDRangel::SWGDeviceSettings& response, + const FOBOSSettings& settings); + + static void webapiUpdateDeviceSettings( + FOBOSSettings& settings, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response); + +private: + DeviceAPI *m_deviceAPI; + QMutex m_mutex; + FOBOSSettings m_settings; + FOBOSWorker *m_worker; + QThread *m_workerThread; + QString m_deviceDescription; + bool m_running; + const QTimer& m_masterTimer; + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + bool applySettings(const FOBOSSettings& settings, const QList& settingsKeys, bool force); + void webapiReverseSendSettings(const QList& deviceSettingsKeys, const FOBOSSettings& settings, bool force); + void webapiReverseSendStartStop(bool start); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleWorkerBackendStatus(const QString& backend, const QString& details); +}; + +#endif // _FOBOS_FOBOSINPUT_H_ diff --git a/plugins/samplesource/fobos/fobosplugin.cpp b/plugins/samplesource/fobos/fobosplugin.cpp new file mode 100644 index 000000000..530d43868 --- /dev/null +++ b/plugins/samplesource/fobos/fobosplugin.cpp @@ -0,0 +1,145 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2020, 2022-2023 Edouard Griffiths, F4EXB // +// Copyright (C) 2019 Davide Gerhard // +// Copyright (C) 2020 Kacper Michajłow // +// // +// 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" + +#ifdef SERVER_MODE +#include "FOBOSinput.h" +#else +#include "FOBOSgui.h" +#endif +#include "FOBOSplugin.h" +#include "FOBOSwebapiadapter.h" + +const PluginDescriptor FOBOSPlugin::m_pluginDescriptor = { + QStringLiteral("FOBOS"), + QStringLiteral("Fobos SDR input"), + QStringLiteral("7.25.0"), + QStringLiteral("(c) Edouard Griffiths, F4EXB"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +static constexpr const char* const m_hardwareID = "FobosSDR"; +static constexpr const char* const m_deviceTypeID = FOBOS_DEVICE_TYPE_ID; + +FOBOSPlugin::FOBOSPlugin(QObject* parent) : + QObject(parent) +{ +} + +const PluginDescriptor& FOBOSPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void FOBOSPlugin::initPlugin(PluginAPI* pluginAPI) +{ + pluginAPI->registerSampleSource(m_deviceTypeID, this); +} + +void FOBOSPlugin::enumOriginDevices(QStringList& listedHwIds, OriginDevices& originDevices) +{ + if (listedHwIds.contains(m_hardwareID)) { // check if it was done + return; + } + + originDevices.append(OriginDevice("Fobos SDR", + m_hardwareID, + QString(), + 0, + 1, // nb Rx + 0 // nb Tx + )); + + listedHwIds.append(m_hardwareID); +} + +PluginInterface::SamplingDevices FOBOSPlugin::enumSampleSources(const OriginDevices& originDevices) +{ + SamplingDevices result; + + for (OriginDevices::const_iterator it = originDevices.begin(); it != originDevices.end(); ++it) + { + if (it->hardwareId == m_hardwareID) + { + result.append(SamplingDevice( + it->displayableName, + m_hardwareID, + m_deviceTypeID, + it->serial, + it->sequence, + PluginInterface::SamplingDevice::BuiltInDevice, + PluginInterface::SamplingDevice::StreamSingleRx, + 1, + 0 + )); + } + } + + return result; +} + +#ifdef SERVER_MODE +DeviceGUI* FOBOSPlugin::createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + (void) sourceId; + (void) widget; + (void) deviceUISet; + return 0; +} +#else +DeviceGUI* FOBOSPlugin::createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + if(sourceId == m_deviceTypeID) { + FOBOSGui* gui = new FOBOSGui(deviceUISet); + *widget = gui; + return gui; + } else { + return 0; + } +} +#endif + +DeviceSampleSource *FOBOSPlugin::createSampleSourcePluginInstance(const QString& sourceId, DeviceAPI *deviceAPI) +{ + if (sourceId == m_deviceTypeID) + { + FOBOSInput* input = new FOBOSInput(deviceAPI); + return input; + } + else + { + return 0; + } +} + +DeviceWebAPIAdapter *FOBOSPlugin::createDeviceWebAPIAdapter() const +{ + return new FOBOSWebAPIAdapter(); +} diff --git a/plugins/samplesource/fobos/fobosplugin.h b/plugins/samplesource/fobos/fobosplugin.h new file mode 100644 index 000000000..e7d4e1dd4 --- /dev/null +++ b/plugins/samplesource/fobos/fobosplugin.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2014 John Greb // +// Copyright (C) 2015-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Kacper Michajłow // +// // +// 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 _FOBOS_FOBOSPLUGIN_H +#define _FOBOS_FOBOSPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class PluginAPI; + +#define FOBOS_DEVICE_TYPE_ID "sdrangel.samplesource.FOBOS" + +class FOBOSPlugin : public QObject, public PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID FOBOS_DEVICE_TYPE_ID) + +public: + explicit FOBOSPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void enumOriginDevices(QStringList& listedHwIds, OriginDevices& originDevices); + virtual SamplingDevices enumSampleSources(const OriginDevices& originDevices); + virtual DeviceGUI* createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet); + virtual DeviceSampleSource* createSampleSourcePluginInstance(const QString& sourceId, DeviceAPI *deviceAPI); + virtual DeviceWebAPIAdapter* createDeviceWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; +}; + +#endif // _FOBOS_FOBOSPLUGIN_H diff --git a/plugins/samplesource/fobos/fobossettings.cpp b/plugins/samplesource/fobos/fobossettings.cpp new file mode 100644 index 000000000..57c19177e --- /dev/null +++ b/plugins/samplesource/fobos/fobossettings.cpp @@ -0,0 +1,337 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020, 2022 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 "FOBOSsettings.h" + +FOBOSSettings::FOBOSSettings() +{ + resetToDefaults(); +} + +void FOBOSSettings::resetToDefaults() +{ + m_title = "Fobos SDR"; + m_centerFrequency = 104000*1000; + m_frequencyShift = 0; + m_sampleRate = 25000*1000; + m_log2Decim = 0; + m_fcPos = FC_POS_CENTER; + m_sampleSizeIndex = 0; + m_amplitudeBits = 3200; + m_autoCorrOptions = AutoCorrDCAndIQ; + m_modulation = static_cast(8); + m_modulationTone = 44; // 440 Hz + m_amModulation = 50; // 50% + m_fmDeviation = 50; // 5 kHz + m_dcFactor = 0.0f; + m_iFactor = 0.0f; + m_qFactor = 0.0f; + m_phaseImbalance = 0.0f; + m_inputMode = InputRF; + m_bandwidthPercent = 90; + m_gpoMask = 0; + m_externalClock = false; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; +} + +QByteArray FOBOSSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_title); + s.writeS32(2, m_frequencyShift); + s.writeU32(3, m_sampleRate); + s.writeU32(4, m_log2Decim); + s.writeS32(5, (int) m_fcPos); + s.writeU32(6, m_sampleSizeIndex); + s.writeS32(7, m_amplitudeBits); + s.writeS32(8, (int) m_autoCorrOptions); + s.writeFloat(10, m_dcFactor); + s.writeFloat(11, m_iFactor); + s.writeFloat(12, m_qFactor); + s.writeFloat(13, m_phaseImbalance); + s.writeS32(14, (int) m_modulation); + s.writeS32(15, m_modulationTone); + s.writeS32(16, m_amModulation); + s.writeS32(17, m_fmDeviation); + s.writeBool(18, m_useReverseAPI); + s.writeString(19, m_reverseAPIAddress); + s.writeU32(20, m_reverseAPIPort); + s.writeU32(21, m_reverseAPIDeviceIndex); + s.writeS32(22, (int) m_inputMode); + s.writeU32(23, m_bandwidthPercent); + s.writeU32(24, m_gpoMask); + s.writeBool(25, m_externalClock); + + return s.final(); +} + +bool FOBOSSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + int intval; + uint32_t utmp; + + d.readString(1, &m_title, "Fobos SDR"); + d.readS32(2, &m_frequencyShift, 0); + d.readU32(3, &m_sampleRate, 25000*1000); + d.readU32(4, &m_log2Decim, 0); + d.readS32(5, &intval, 0); + m_fcPos = (fcPos_t) intval; + d.readU32(6, &m_sampleSizeIndex, 2); + d.readS32(7, &m_amplitudeBits, 3200); + d.readS32(8, &intval, 0); + + if (intval < 0 || intval > 2) { + m_autoCorrOptions = AutoCorrDCAndIQ; + } else { + m_autoCorrOptions = static_cast(intval); + } + + d.readFloat(10, &m_dcFactor, 0.0f); + d.readFloat(11, &m_iFactor, 0.0f); + d.readFloat(12, &m_qFactor, 0.0f); + d.readFloat(13, &m_phaseImbalance, 0.0f); + d.readS32(14, &intval, 0); + + if (intval < 0 || intval > (int) ModulationLast) { + m_modulation = static_cast(8); + } else { + m_modulation = (Modulation) intval; + } + + d.readS32(15, &m_modulationTone, 44); + d.readS32(16, &m_amModulation, 50); + d.readS32(17, &m_fmDeviation, 50); + + d.readBool(18, &m_useReverseAPI, false); + d.readString(19, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(20, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(21, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + + d.readS32(22, &intval, 0); + if (intval < 0 || intval > 3) { + m_inputMode = InputRF; + } else { + m_inputMode = (InputMode) intval; + } + + d.readU32(23, &utmp, 90); + if ((utmp >= 20 && utmp <= 100 && (utmp % 10) == 0)) { + m_bandwidthPercent = utmp; + } else { + m_bandwidthPercent = 90; + } + + d.readU32(24, &utmp, 0); + m_gpoMask = utmp & 0xffu; + d.readBool(25, &m_externalClock, false); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void FOBOSSettings::applySettings(const QStringList& settingsKeys, const FOBOSSettings& settings) +{ + if (settingsKeys.contains("title")) { + m_title = settings.m_title; + } + if (settingsKeys.contains("centerFrequency")) { + m_centerFrequency = settings.m_centerFrequency; + } + if (settingsKeys.contains("frequencyShift")) { + m_frequencyShift = settings.m_frequencyShift; + } + if (settingsKeys.contains("sampleRate")) { + m_sampleRate = settings.m_sampleRate; + } + if (settingsKeys.contains("log2Decim")) { + m_log2Decim = settings.m_log2Decim; + } + if (settingsKeys.contains("fcPos")) { + m_fcPos = settings.m_fcPos; + } + if (settingsKeys.contains("sampleSizeIndex")) { + m_sampleSizeIndex = settings.m_sampleSizeIndex; + } + if (settingsKeys.contains("amplitudeBits")) { + m_amplitudeBits = settings.m_amplitudeBits; + } + if (settingsKeys.contains("autoCorrOptions")) { + m_autoCorrOptions = settings.m_autoCorrOptions; + } + if (settingsKeys.contains("modulation")) { + m_modulation = settings.m_modulation; + } + if (settingsKeys.contains("modulationTone")) { + m_modulationTone = settings.m_modulationTone; + } + if (settingsKeys.contains("amModulation")) { + m_amModulation = settings.m_amModulation; + } + if (settingsKeys.contains("fmDeviation")) { + m_fmDeviation = settings.m_fmDeviation; + } + if (settingsKeys.contains("dcFactor")) { + m_dcFactor = settings.m_dcFactor; + } + if (settingsKeys.contains("iFactor")) { + m_iFactor = settings.m_iFactor; + } + if (settingsKeys.contains("qFactor")) { + m_qFactor = settings.m_qFactor; + } + if (settingsKeys.contains("phaseImbalance")) { + m_phaseImbalance = settings.m_phaseImbalance; + } + if (settingsKeys.contains("useReverseAPI")) { + m_useReverseAPI = settings.m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress")) { + m_reverseAPIAddress = settings.m_reverseAPIAddress; + } + if (settingsKeys.contains("reverseAPIPort")) { + m_reverseAPIPort = settings.m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIDeviceIndex")) { + m_reverseAPIDeviceIndex = settings.m_reverseAPIDeviceIndex; + } + if (settingsKeys.contains("inputMode")) { + m_inputMode = settings.m_inputMode; + } + if (settingsKeys.contains("bandwidthPercent")) { + m_bandwidthPercent = settings.m_bandwidthPercent; + } + if (settingsKeys.contains("gpoMask")) { + m_gpoMask = settings.m_gpoMask & 0xffu; + } + if (settingsKeys.contains("externalClock")) { + m_externalClock = settings.m_externalClock; + } +} + +QString FOBOSSettings::getDebugString(const QStringList& settingsKeys, bool force) const +{ + std::ostringstream ostr; + + if (settingsKeys.contains("title") || force) { + ostr << " m_title: " << m_title.toStdString(); + } + if (settingsKeys.contains("centerFrequency") || force) { + ostr << " m_centerFrequency: " << m_centerFrequency; + } + if (settingsKeys.contains("frequencyShift") || force) { + ostr << " m_frequencyShift: " << m_frequencyShift; + } + if (settingsKeys.contains("sampleRate") || force) { + ostr << " m_sampleRate: " << m_sampleRate; + } + if (settingsKeys.contains("log2Decim") || force) { + ostr << " m_log2Decim: " << m_log2Decim; + } + if (settingsKeys.contains("fcPos") || force) { + ostr << " m_fcPos: " << m_fcPos; + } + if (settingsKeys.contains("sampleSizeIndex") || force) { + ostr << " m_sampleSizeIndex: " << m_sampleSizeIndex; + } + if (settingsKeys.contains("amplitudeBits") || force) { + ostr << " m_amplitudeBits: " << m_amplitudeBits; + } + if (settingsKeys.contains("autoCorrOptions") || force) { + ostr << " m_autoCorrOptions: " << m_autoCorrOptions; + } + if (settingsKeys.contains("modulation") || force) { + ostr << " m_modulation: " << m_modulation; + } + if (settingsKeys.contains("modulationTone") || force) { + ostr << " m_modulationTone: " << m_modulationTone; + } + if (settingsKeys.contains("amModulation") || force) { + ostr << " m_amModulation: " << m_amModulation; + } + if (settingsKeys.contains("fmDeviation") || force) { + ostr << " m_fmDeviation: " << m_fmDeviation; + } + if (settingsKeys.contains("dcFactor") || force) { + ostr << " m_dcFactor: " << m_dcFactor; + } + if (settingsKeys.contains("iFactor") || force) { + ostr << " m_iFactor: " << m_iFactor; + } + if (settingsKeys.contains("qFactor") || force) { + ostr << " m_qFactor: " << m_qFactor; + } + if (settingsKeys.contains("phaseImbalance") || force) { + ostr << " m_phaseImbalance: " << m_phaseImbalance; + } + if (settingsKeys.contains("useReverseAPI") || force) { + ostr << " m_useReverseAPI: " << m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress") || force) { + ostr << " m_reverseAPIAddress: " << m_reverseAPIAddress.toStdString(); + } + if (settingsKeys.contains("reverseAPIPort") || force) { + ostr << " m_reverseAPIPort: " << m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIDeviceIndex") || force) { + ostr << " m_reverseAPIDeviceIndex: " << m_reverseAPIDeviceIndex; + } + if (settingsKeys.contains("inputMode") || force) { + ostr << " m_inputMode: " << (int) m_inputMode; + } + if (settingsKeys.contains("bandwidthPercent") || force) { + ostr << " m_bandwidthPercent: " << m_bandwidthPercent; + } + if (settingsKeys.contains("gpoMask") || force) { + ostr << " m_gpoMask: " << m_gpoMask; + } + if (settingsKeys.contains("externalClock") || force) { + ostr << " m_externalClock: " << m_externalClock; + } + + return QString(ostr.str().c_str()); +} diff --git a/plugins/samplesource/fobos/fobossettings.h b/plugins/samplesource/fobos/fobossettings.h new file mode 100644 index 000000000..e073a0db3 --- /dev/null +++ b/plugins/samplesource/fobos/fobossettings.h @@ -0,0 +1,102 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2014 John Greb // +// Copyright (C) 2015, 2017-2020, 2022 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 _FOBOS_FOBOSSETTINGS_H_ +#define _FOBOS_FOBOSSETTINGS_H_ + +#include + +struct FOBOSSettings { + typedef enum { + FC_POS_INFRA = 0, + FC_POS_SUPRA, + FC_POS_CENTER + } fcPos_t; + + typedef enum { + AutoCorrNone, + AutoCorrDC, + AutoCorrDCAndIQ, + AutoCorrLast, + } AutoCorrOptions; + + typedef enum { + InputRF = 0, + InputIQDirect, + InputHF1Direct, + InputHF2Direct + } InputMode; + + // Reuse this legacy field as Fobos VGA gain index 0..31. + // Keep the old enum name to avoid larger SDRangel/WebAPI changes in this UI-only iteration. + typedef enum { + ModulationNone = 0, + ModulationAM = 1, + ModulationFM = 2, + ModulationPattern0 = 3, + ModulationPattern1 = 4, + ModulationPattern2 = 5, + ModulationLast = 31 + } Modulation; + + QString m_title; + quint64 m_centerFrequency; + qint32 m_frequencyShift; + quint32 m_sampleRate; + quint32 m_log2Decim; + fcPos_t m_fcPos; + quint32 m_sampleSizeIndex; + qint32 m_amplitudeBits; + AutoCorrOptions m_autoCorrOptions; + Modulation m_modulation; + int m_modulationTone; //!< 10'Hz + int m_amModulation; //!< percent + int m_fmDeviation; //!< 100'Hz + float m_dcFactor; //!< -1.0 < x < 1.0 + float m_iFactor; //!< -1.0 < x < 1.0 + float m_qFactor; //!< -1.0 < x < 1.0 + float m_phaseImbalance; //!< -1.0 < x < 1.0 + + // uSDR-style Fobos operator settings. + // These fields are applied through the Agile API either live or through controlled restart, + // depending on whether the setting can be safely changed while streaming. + InputMode m_inputMode; + quint32 m_bandwidthPercent; //!< 0 = Auto, otherwise relative bandwidth percent such as 20/50/80/90 + quint32 m_gpoMask; //!< bit mask for GPO 0..7 + bool m_externalClock; + + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + + FOBOSSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void applySettings(const QStringList& settingsKeys, const FOBOSSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; +}; + + + + + +#endif /* _FOBOS_FOBOSSETTINGS_H_ */ diff --git a/plugins/samplesource/fobos/foboswebapiadapter.cpp b/plugins/samplesource/fobos/foboswebapiadapter.cpp new file mode 100644 index 000000000..0bdf7bc30 --- /dev/null +++ b/plugins/samplesource/fobos/foboswebapiadapter.cpp @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2021 Edouard Griffiths, F4EXB // +// // +// Implementation of static web API adapters used for preset serialization and // +// deserialization // +// // +// 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 "SWGDeviceSettings.h" +#include "FOBOSinput.h" +#include "FOBOSwebapiadapter.h" + +FOBOSWebAPIAdapter::FOBOSWebAPIAdapter() +{} + +FOBOSWebAPIAdapter::~FOBOSWebAPIAdapter() +{} + +int FOBOSWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setTestSourceSettings(new SWGSDRangel::SWGTestSourceSettings()); + response.getTestSourceSettings()->init(); + FOBOSInput::webapiFormatDeviceSettings(response, m_settings); + return 200; +} + +int FOBOSWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + FOBOSInput::webapiUpdateDeviceSettings(m_settings, deviceSettingsKeys, response); + return 200; +} diff --git a/plugins/samplesource/fobos/foboswebapiadapter.h b/plugins/samplesource/fobos/foboswebapiadapter.h new file mode 100644 index 000000000..f6a9b0726 --- /dev/null +++ b/plugins/samplesource/fobos/foboswebapiadapter.h @@ -0,0 +1,46 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2017, 2019 Edouard Griffiths, F4EXB // +// // +// Implementation of static web API adapters used for preset serialization and // +// deserialization // +// // +// 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 "device/devicewebapiadapter.h" +#include "FOBOSsettings.h" + +class FOBOSWebAPIAdapter : public DeviceWebAPIAdapter +{ +public: + FOBOSWebAPIAdapter(); + virtual ~FOBOSWebAPIAdapter(); + virtual QByteArray serialize() { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage); + +private: + FOBOSSettings m_settings; +}; diff --git a/plugins/samplesource/fobos/fobosworker.cpp b/plugins/samplesource/fobos/fobosworker.cpp new file mode 100644 index 000000000..30812d6f8 --- /dev/null +++ b/plugins/samplesource/fobos/fobosworker.cpp @@ -0,0 +1,1284 @@ +/////////////////////////////////////////////////////////////////////////////////// +// SDRangel Fobos SDR native source backend +// Auto backend selection: Agile API first, regular/classic API fallback. +// Runtime DLLs are loaded dynamically so SDRangel can still build without the Fobos SDK. +/////////////////////////////////////////////////////////////////////////////////// + +#include "fobosworker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace +{ +#if defined(FOBOS_DEBUG_FILE_LOG) + static const char* kLogPath = "sdrangel_fobos_source.log"; +#endif + static const char* kAgileDll = "fobos_sdr.dll"; + static const char* kRegularDll = "fobos.dll"; + static const uint32_t kSyncComplexBufferLength = 65536; // Sync read block size, about 8.2 ms at 8 MS/s + static const uint32_t kSyncFloatStorage = kSyncComplexBufferLength * 2u; // interleaved float I/Q + static const uint32_t kFifoChunkComplexLength = kSyncComplexBufferLength; // One FIFO write per read block + static const unsigned int kStartupProbeMaxAttempts = 8; // Verify read_sync before exposing a running stream + static const unsigned int kStartupReaderErrorAbort = 32; // avoid endless dev==NULL read loop after a failed start + static const double kDefaultFrequencyHz = 104000000.0; + static const int kDefaultSampleRateHz = 25000000; + static const unsigned int kDefaultLna = 2; + static const unsigned int kDefaultVga = 8; + static const double kDefaultIqGain = 32.0; + static const int kDefaultInputMode = 0; // RF + static const unsigned int kDefaultBandwidthPercent = 90; + static const unsigned int kDefaultGpoMask = 0; + static const bool kDefaultExternalClock = false; + +#ifdef _WIN32 + static QMutex g_sessionMutex; + static HMODULE g_agileLibrary = nullptr; // DLL is process-scoped; device is not. + static HMODULE g_regularLibrary = nullptr; // regular/classic DLL, process-scoped as well. + static bool g_deviceBusy = false; // prevent two SDRangel device sets from using one Fobos. + + static void formatLastError(DWORD err, char* out, size_t outSize) + { + if (!out || outSize == 0) { + return; + } + out[0] = '\0'; + DWORD n = FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + err, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + out, + static_cast(outSize), + nullptr); + if (n == 0) { + std::snprintf(out, outSize, "FormatMessage failed"); + } else { + while (n > 0 && (out[n-1] == '\r' || out[n-1] == '\n' || out[n-1] == ' ' || out[n-1] == '\t')) { + out[n-1] = '\0'; + --n; + } + } + } +#endif +} + +FOBOSWorker::FOBOSWorker(SampleSinkFifo* sampleFifo, QObject* parent) : + QObject(parent), + m_sampleFifo(sampleFifo), + m_convertBuffer(kSyncComplexBufferLength), + m_centerFrequencyHz(static_cast(kDefaultFrequencyHz)), + m_samplerate(kDefaultSampleRateHz), + m_log2Decim(0), + m_fcPos(0), + m_frequencyShift(0), + m_lnaGain(kDefaultLna), + m_vgaGain(kDefaultVga), + m_inputMode(kDefaultInputMode), + m_bandwidthPercent(kDefaultBandwidthPercent), + m_gpoMask(kDefaultGpoMask), + m_externalClock(kDefaultExternalClock), + m_iqGain(kDefaultIqGain), + m_runtimeInputMode(kDefaultInputMode), + m_gainDiagActive(false), + m_gainDiagCollecting(false), + m_gainDiagKind(), + m_gainDiagValue(0), + m_gainDiagSkipBuffers(0), + m_gainDiagTargetBuffers(0), + m_gainDiagCollectedBuffers(0), + m_gainDiagSamples(0), + m_gainDiagPower(0.0), + m_gainDiagPeak(0.0), + m_running(false), + m_stopRequested(false), + m_syncStarted(false), + m_readerActive(false), + m_readerFinished(false), + m_dev(nullptr), + m_runtimeBackend(FobosRuntimeBackend::None) +#ifdef _WIN32 + , m_libraryHandle(nullptr), + m_errorName(nullptr), + m_closeDev(nullptr), + m_readSync(nullptr), + m_stopSync(nullptr), + m_setFrequency(nullptr), + m_setSamplerate(nullptr), + m_getSamplerates(nullptr), + m_setDirectSampling(nullptr), + m_setAutoBandwidth(nullptr), + m_setUserGpo(nullptr), + m_setClkSource(nullptr), + m_setLnaGain(nullptr), + m_setVgaGain(nullptr), + m_regularSetFrequency(nullptr), + m_regularSetSamplerate(nullptr), + m_regularGetSamplerates(nullptr), + m_regularSetDirectSampling(nullptr), + m_regularSetUserGpo(nullptr), + m_regularSetClkSource(nullptr), + m_regularSetLnaGain(nullptr), + m_regularSetVgaGain(nullptr) +#endif + , m_totalReads(0), + m_totalSamples(0), + m_totalWritten(0), + m_failedReads(0) +{ +} + +FOBOSWorker::~FOBOSWorker() +{ + stopWork(); +} + +void FOBOSWorker::startWork() +{ + bool expected = false; + if (!m_running.compare_exchange_strong(expected, true)) { + return; + } + + qInfo() << "FOBOSWorker::startWork: Fobos SDR sync source starting"; + emit backendStatusChanged(QStringLiteral("Probing"), QStringLiteral("Backend mode: Auto. Trying Agile API first, then regular API.")); + if (runAgileStart()) { + qInfo() << "FOBOSWorker::startWork: Agile sync source started"; + return; + } + + if (!m_stopRequested.load()) { + m_running.store(true); + qInfo() << "FOBOSWorker::startWork: Agile not available, trying regular Fobos API"; + if (runRegularStart()) { + qInfo() << "FOBOSWorker::startWork: Regular sync source started"; + return; + } + } + + qInfo() << "FOBOSWorker::startWork: no usable Fobos backend started"; + emit backendStatusChanged(QStringLiteral("No device"), QStringLiteral("No Fobos SDR device was found through Agile or regular API.")); +} + +void FOBOSWorker::stopWork() +{ + emit backendStatusChanged(QStringLiteral("Stopped"), QStringLiteral("Fobos source is stopped. Backend will be detected on next Start.")); + m_stopRequested.store(true); + m_running.store(false); + +#ifdef _WIN32 + bool readerJoined = true; + + // Do not call stop_sync while read_sync is active. + // The reader loop checks m_stopRequested after every successful read_sync and exits by itself. + // stop_sync is called only after the reader thread has returned. This avoids driver/API + // races between a blocking read_sync call and stop_sync. + if (m_readerThread.joinable()) { + logLine("reader_join=START strategy=stop_flag_then_join_before_stop_sync"); + m_readerThread.join(); + readerJoined = true; + logLine("reader_join=RETURN joined=YES before_stop_sync"); + } + + fobos_dev_t* dev = m_dev.load(); + if (dev && m_stopSync && m_syncStarted.exchange(false)) { + logLine("stopWork_stop_sync=START_AFTER_READER_JOIN"); + int r = m_stopSync(dev); + logLine("stopWork_stop_sync=RETURN_AFTER_READER_JOIN result=%d error='%s'", r, errorName(r)); + } else { + logLine("stopWork_stop_sync=SKIPPED no_dev_or_not_started"); + } + + cleanupAfterReaderJoined(readerJoined); +#endif +} + +void FOBOSWorker::setSamplerate(int samplerate) +{ + QMutexLocker locker(&m_settingsMutex); + m_samplerate = samplerate > 0 ? samplerate : kDefaultSampleRateHz; + if (m_running.load()) { + logLine("live_sample_rate_change=CONTROLLED_RESTART_REQUIRED_BUT_NOT_HANDLED_HERE requested_hz=%d", m_samplerate); + } +} + +void FOBOSWorker::setCenterFrequency(uint64_t centerFrequencyHz) +{ + QMutexLocker locker(&m_settingsMutex); + m_centerFrequencyHz = centerFrequencyHz > 0 ? centerFrequencyHz : static_cast(kDefaultFrequencyHz); + fobos_dev_t* dev = m_dev.load(); + if (dev && m_runtimeInputMode.load() == 0) { + int r = callSetFrequency(dev, static_cast(m_centerFrequencyHz)); + logLine("live_frequency_call=RETURN result=%d error='%s' requested_hz=%llu", r, errorName(r), static_cast(m_centerFrequencyHz)); + } else if (m_running.load()) { + logLine("live_frequency_call=SKIPPED mode=%d no_dev_or_direct_sampling", m_runtimeInputMode.load()); + } +} + +void FOBOSWorker::setLog2Decimation(unsigned int log2_decim) +{ + QMutexLocker locker(&m_settingsMutex); + m_log2Decim = log2_decim; +} + +void FOBOSWorker::setFcPos(int fcPos) +{ + QMutexLocker locker(&m_settingsMutex); + m_fcPos = fcPos; +} + +void FOBOSWorker::setBitSize(uint32_t) {} +void FOBOSWorker::setAmplitudeBits(int32_t amplitudeBits) +{ + double gain = static_cast(amplitudeBits) / 100.0; + if (gain < 0.01) { + gain = 0.01; + } else if (gain > 512.0) { + gain = 512.0; + } + m_iqGain.store(gain); + if (m_running.load()) { logLine("live_iq_gain_set=%.3f", gain); } +} +void FOBOSWorker::setLnaGain(unsigned int lnaGain) +{ + QMutexLocker locker(&m_settingsMutex); + m_lnaGain = lnaGain > 2u ? 2u : lnaGain; + fobos_dev_t* dev = m_dev.load(); + if (dev) { + int r = callSetLnaGain(dev, m_lnaGain); + logLine("live_lna_gain_call=RETURN result=%d error='%s' value=%u", r, errorName(r), m_lnaGain); + } +} +void FOBOSWorker::setVgaGain(unsigned int vgaGain) +{ + QMutexLocker locker(&m_settingsMutex); + m_vgaGain = vgaGain > 31u ? 31u : vgaGain; + fobos_dev_t* dev = m_dev.load(); + if (dev) { + int r = callSetVgaGain(dev, m_vgaGain); + logLine("live_vga_gain_call=RETURN result=%d error='%s' value=%u", r, errorName(r), m_vgaGain); + } +} +void FOBOSWorker::setInputMode(int inputMode) +{ + QMutexLocker locker(&m_settingsMutex); + if (inputMode < 0 || inputMode > 3) { + inputMode = kDefaultInputMode; + } + m_inputMode = inputMode; + if (m_running.load()) { + logLine("live_input_mode_change=CONTROLLED_RESTART_REQUIRED_BUT_NOT_HANDLED_HERE input_mode=%d", inputMode); + } +} + +void FOBOSWorker::setBandwidthPercent(unsigned int bandwidthPercent) +{ + QMutexLocker locker(&m_settingsMutex); + if (!(bandwidthPercent >= 20 && bandwidthPercent <= 100 && (bandwidthPercent % 10) == 0)) { + bandwidthPercent = kDefaultBandwidthPercent; + } + m_bandwidthPercent = bandwidthPercent; + fobos_dev_t* dev = m_dev.load(); + if (dev) { + int r = callSetAutoBandwidth(dev, m_bandwidthPercent); + logLine("live_auto_bandwidth_call=RETURN result=%d error='%s' percent=%u", r, errorName(r), m_bandwidthPercent); + } +} + +void FOBOSWorker::setGpoMask(unsigned int gpoMask) +{ + QMutexLocker locker(&m_settingsMutex); + m_gpoMask = gpoMask & 0xffu; + fobos_dev_t* dev = m_dev.load(); + if (dev) { + int r = callSetGpo(dev, m_gpoMask); + logLine("live_gpo_call=RETURN result=%d error='%s' mask=0x%02X", r, errorName(r), m_gpoMask); + } +} + +void FOBOSWorker::setExternalClock(bool externalClock) +{ + QMutexLocker locker(&m_settingsMutex); + m_externalClock = externalClock; + if (m_running.load()) { + logLine("live_external_clock_change=CONTROLLED_RESTART_REQUIRED_BUT_NOT_HANDLED_HERE external_clock=%s", externalClock ? "true" : "false"); + } +} + +void FOBOSWorker::setDCFactor(float) {} +void FOBOSWorker::setIFactor(float) {} +void FOBOSWorker::setQFactor(float) {} +void FOBOSWorker::setPhaseImbalance(float) {} +void FOBOSWorker::setFrequencyShift(int shift) { QMutexLocker locker(&m_settingsMutex); m_frequencyShift = shift; } +void FOBOSWorker::setToneFrequency(int) {} +void FOBOSWorker::setModulation(int modulation) +{ + // Legacy modulation combo is now the uSDR-style VGA gain control. + // Direct range is 0..31. + if (modulation < 0) { + modulation = 0; + } else if (modulation > 31) { + modulation = 31; + } + setVgaGain(static_cast(modulation)); +} +void FOBOSWorker::setAMModulation(float) {} +void FOBOSWorker::setFMDeviation(float) {} +void FOBOSWorker::setPattern0() {} +void FOBOSWorker::setPattern1() {} +void FOBOSWorker::setPattern2() {} + + + +bool FOBOSWorker::runAgileStart() +{ +#ifdef _WIN32 + logLine("============================================================"); + logLine("SDRangel Fobos SDR Agile native source"); + logTimestamp(); + logLine("mode=agile_sync_to_sdrangel_fifo"); + logLine("lifecycle_policy=stopflag_join_then_stopsync_close"); + logLine("stream_policy=stable_fast_float_to_fix_single_write_no_decimation"); + logLine("agile_dll=%s", kAgileDll); + + uint64_t centerFrequency = 0; + int sampleRate = 0; + unsigned int lnaGain = kDefaultLna; + unsigned int vgaGain = kDefaultVga; + int inputMode = kDefaultInputMode; + unsigned int bandwidthPercent = kDefaultBandwidthPercent; + unsigned int gpoMask = kDefaultGpoMask; + bool externalClock = kDefaultExternalClock; + { + QMutexLocker locker(&m_settingsMutex); + centerFrequency = m_centerFrequencyHz; + sampleRate = m_samplerate > 0 ? m_samplerate : kDefaultSampleRateHz; + lnaGain = m_lnaGain; + vgaGain = m_vgaGain; + inputMode = m_inputMode; + bandwidthPercent = m_bandwidthPercent; + gpoMask = m_gpoMask; + externalClock = m_externalClock; + } + + logLine("requested_center_frequency_hz=%llu", static_cast(centerFrequency)); + logLine("requested_sample_rate_hz=%d", sampleRate); + logLine("requested_lna=%u", lnaGain); + logLine("requested_vga=%u", vgaGain); + logLine("requested_iq_gain=%.3f", m_iqGain.load()); + logLine("requested_input_mode=%d", inputMode); + logLine("requested_bandwidth_percent=%u", bandwidthPercent); + logLine("requested_gpo_mask=0x%02X", gpoMask); + logLine("requested_external_clock=%s", externalClock ? "true" : "false"); + HMODULE lib = nullptr; + + { + QMutexLocker sessionLock(&g_sessionMutex); + + if (g_deviceBusy) { + logLine("device_busy_guard=ACTIVE another_FOBOS_instance_is_already_streaming"); + logLine("source_result=REFUSED_DEVICE_ALREADY_BUSY"); + m_running.store(false); + return false; + } + + if (!g_agileLibrary) { + // Resolve fobos_sdr.dll using the normal Windows DLL search order. + // No developer-machine paths are used here; distribution packages should place + // fobos_sdr.dll and its runtime dependencies next to the SDRangel executable, + // or the user may expose them through PATH. + SetLastError(0); + g_agileLibrary = LoadLibraryA(kAgileDll); + DWORD gle = GetLastError(); + logLine("LoadLibrary=%s handle=0x%p gle=%lu", g_agileLibrary ? "OK" : "FAILED", g_agileLibrary, gle); + if (!g_agileLibrary) { + char errText[512] = {}; + formatLastError(gle, errText, sizeof(errText)); + logLine("LoadLibrary_error='%s'", errText); + logLine("operator_action=install_fobos_sdr_dll_next_to_sdrangel_or_add_it_to_PATH_then_restart_SDRangel"); + m_running.store(false); + return false; + } + } else { + logLine("LoadLibrary=REUSE_PROCESS_SCOPED handle=0x%p", g_agileLibrary); + } + + lib = g_agileLibrary; + g_deviceBusy = true; + } + + m_libraryHandle = lib; + m_runtimeBackend = FobosRuntimeBackend::Agile; + + auto getApiInfo = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_api_info")); + auto getDeviceCount = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_device_count")); + auto listDevices = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_list_devices")); + auto openDev = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_open")); + m_closeDev = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_close")); + auto getBoardInfo = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_board_info")); + m_errorName = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_error_name")); + m_setFrequency = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_frequency")); + auto setFrequency = m_setFrequency; + m_setSamplerate = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_samplerate")); + m_getSamplerates = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_samplerates")); + m_setDirectSampling = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_direct_sampling")); + m_setAutoBandwidth = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_auto_bandwidth")); + m_setUserGpo = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_user_gpo")); + m_setClkSource = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_clk_source")); + auto setSamplerate = m_setSamplerate; + m_setLnaGain = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_lna_gain")); + m_setVgaGain = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_vga_gain")); + auto setLnaGain = m_setLnaGain; + auto setVgaGain = m_setVgaGain; + auto startSync = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_start_sync")); + m_readSync = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_read_sync")); + m_stopSync = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_stop_sync")); + + bool ok = getApiInfo && getDeviceCount && listDevices && openDev && m_closeDev && getBoardInfo && m_errorName && setFrequency && setSamplerate && setLnaGain && setVgaGain && startSync && m_readSync && m_stopSync; + logLine("exports_ok=%s", ok ? "YES" : "NO"); + if (!ok) { + logLine("source_result=FAILED_MISSING_AGILE_EXPORTS"); + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + m_running.store(false); + return false; + } + + char libVersion[128] = {}; + char drvVersion[128] = {}; + int rInfo = getApiInfo(libVersion, drvVersion); + logLine("api_info_call=result=%d lib='%s' drv='%s'", rInfo, libVersion, drvVersion); + + int count = getDeviceCount(); + logLine("device_count_call=OK count=%d", count); + if (count <= 0) { + logLine("device_detect_hint=NO_FOBOS_DEVICE_VISIBLE_TO_AGILE_API"); + logLine("device_busy_hint=check_USB_connection_and_close_microSDR_uSDR_other_SDRangel_instances"); + logLine("source_result=FAILED_NO_DEVICE_VISIBLE"); + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + m_running.store(false); + return false; + } + + char serials[4096] = {}; + int rList = listDevices(serials); + logLine("list_devices_call=RETURN result=%d serials='%s'", rList, serials); + + fobos_dev_t* dev = nullptr; + int r = openDev(&dev, 0); + logLine("open_call=RETURN result=%d error='%s' dev=0x%p", r, errorName(r), dev); + if ((r != 0) || !dev) { + logLine("device_open_hint=FAILED_TO_OPEN_FOBOS_INDEX_0"); + logLine("device_busy_hint=Fobos_may_be_owned_by_microSDR_uSDR_or_another_SDRangel_instance"); + logLine("operator_action=close_other_Fobos_software_then_Stop_Start_or_restart_SDRangel"); + logLine("source_result=FAILED_OPEN_DEVICE_BUSY_OR_UNAVAILABLE"); + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + m_running.store(false); + return false; + } + m_dev.store(dev); + + char hw[128] = {}, fw[128] = {}, manufacturer[128] = {}, product[128] = {}, serial[128] = {}; + int rBoard = getBoardInfo(dev, hw, fw, manufacturer, product, serial); + logLine("board_info_call=RETURN result=%d error='%s'", rBoard, errorName(rBoard)); + logLine("board_hw_revision='%s'", hw); + logLine("board_fw_version='%s'", fw); + logLine("board_manufacturer='%s'", manufacturer); + logLine("board_product='%s'", product); + logLine("board_serial='%s'", serial); + const QString agileBackendDetails = QStringLiteral("Fobos Agile API %1; driver %2; HW %3; FW %4; serial %5") + .arg(QString::fromLocal8Bit(libVersion)) + .arg(QString::fromLocal8Bit(drvVersion)) + .arg(QString::fromLocal8Bit(hw)) + .arg(QString::fromLocal8Bit(fw)) + .arg(QString::fromLocal8Bit(serial)); + + if (m_getSamplerates) { + double rates[32] = {}; + uint32_t rateCount = 32; + int rRates = m_getSamplerates(dev, rates, &rateCount); + logLine("get_samplerates_call=RETURN result=%d count=%u", rRates, rateCount); + for (uint32_t i = 0; (rRates == 0) && (i < rateCount) && (i < 32); ++i) { + logLine("supported_samplerate[%u]=%.0f", i, rates[i]); + } + } else { + logLine("get_samplerates_call=SKIPPED missing_export fallback_list=8000000,10000000,12500000,16000000,20000000,25000000,32000000,40000000,50000000"); + } + + const int direct = (inputMode == 0) ? 0 : 1; + if (m_setDirectSampling) { + r = m_setDirectSampling(dev, direct); + logLine("set_direct_sampling_call=RETURN result=%d error='%s' input_mode=%d direct=%d", r, errorName(r), inputMode, direct); + } else { + logLine("set_direct_sampling_call=SKIPPED missing_export input_mode=%d direct=%d", inputMode, direct); + } + m_runtimeInputMode.store(inputMode); + + if (m_setClkSource) { + r = m_setClkSource(dev, externalClock ? 1 : 0); + logLine("set_clk_source_call=RETURN result=%d error='%s' external_clock=%s", r, errorName(r), externalClock ? "true" : "false"); + } else { + logLine("set_clk_source_call=SKIPPED missing_export external_clock=%s", externalClock ? "true" : "false"); + } + + if (m_setUserGpo) { + r = m_setUserGpo(dev, gpoMask); + logLine("set_user_gpo_call=RETURN result=%d error='%s' mask=0x%02X", r, errorName(r), gpoMask); + } else { + logLine("set_user_gpo_call=SKIPPED missing_export mask=0x%02X", gpoMask); + } + + if (m_setAutoBandwidth) { + const double bwRatio = static_cast(bandwidthPercent) * 0.01; + r = m_setAutoBandwidth(dev, bwRatio); + logLine("set_auto_bandwidth_call=RETURN result=%d error='%s' percent=%u ratio=%.3f", r, errorName(r), bandwidthPercent, bwRatio); + } else { + logLine("set_auto_bandwidth_call=SKIPPED missing_export percent=%u", bandwidthPercent); + } + + r = callSetFrequency(dev, static_cast(centerFrequency)); + logLine("set_frequency_call=RETURN result=%d error='%s'", r, errorName(r)); + r = callSetSamplerate(dev, static_cast(sampleRate)); + logLine("set_samplerate_call=RETURN result=%d error='%s'", r, errorName(r)); + r = callSetLnaGain(dev, lnaGain); + logLine("set_lna_gain_call=RETURN result=%d error='%s'", r, errorName(r)); + r = callSetVgaGain(dev, vgaGain); + logLine("set_vga_gain_call=RETURN result=%d error='%s'", r, errorName(r)); + + r = startSync(dev, kSyncComplexBufferLength); + logLine("start_sync_call=RETURN result=%d error='%s' complex_buf_length=%u float_storage=%u fifo_write_complex=%u", r, errorName(r), kSyncComplexBufferLength, kSyncFloatStorage, kFifoChunkComplexLength); + if (r != 0) { + m_dev.store(nullptr); + m_running.store(false); + if (m_closeDev) { + int rc = m_closeDev(dev); + logLine("close_after_start_sync_failure=RETURN result=%d error='%s'", rc, errorName(rc)); + } + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + return false; + } + + // Start/read robustness: + // Some driver/device states can report a successful start while the first read_sync calls + // still fail, for example with a null device handle reported by the runtime. Do not expose + // such a half-started source as Running. + // The first read_sync loop may return repeated errors and + // no samples are delivered. Probe read_sync synchronously, close/release cleanly if the device handle is unusable, + // and let the operator Start again without flooding the log or leaving a stuck channel. + { + std::vector startupProbe(kSyncFloatStorage); + bool startupProbeOk = false; + uint32_t startupProbeActual = 0; + int startupProbeResult = -9999; + + for (unsigned int attempt = 1; attempt <= kStartupProbeMaxAttempts && !m_stopRequested.load(); ++attempt) { + startupProbeActual = 0; + startupProbeResult = m_readSync(dev, startupProbe.data(), &startupProbeActual); + if ((startupProbeResult == 0) && (startupProbeActual > 0)) { + startupProbeOk = true; + logLine("startup_read_probe=OK attempt=%u actual=%u", attempt, startupProbeActual); + break; + } + logLine("startup_read_probe=RETRY attempt=%u result=%d error='%s' actual=%u", + attempt, startupProbeResult, errorName(startupProbeResult), startupProbeActual); + Sleep(20); + } + + if (!startupProbeOk) { + logLine("startup_read_probe=FAILED max_attempts=%u last_result=%d last_error='%s' action=stop_sync_close_release_busy", + kStartupProbeMaxAttempts, startupProbeResult, errorName(startupProbeResult)); + if (m_stopSync) { + int rs = m_stopSync(dev); + logLine("stop_sync_after_startup_probe_failure=RETURN result=%d error='%s'", rs, errorName(rs)); + } + if (m_closeDev) { + int rc = m_closeDev(dev); + logLine("close_after_startup_probe_failure=RETURN result=%d error='%s'", rc, errorName(rc)); + } + m_dev.store(nullptr); + m_syncStarted.store(false); + m_running.store(false); + m_stopRequested.store(false); + { + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + } + logLine("source_result=FAILED_STARTUP_READ_PROBE_NO_SAMPLES_DEVICE_RELEASED"); + return false; + } + } + + m_syncStarted.store(true); + m_readerFinished.store(false); + m_readerActive.store(true); + m_stopRequested.store(false); + m_totalReads = 0; + m_totalSamples = 0; + m_totalWritten = 0; + m_failedReads = 0; + + logLine("reader_thread=STARTING"); + m_readerThread = std::thread(&FOBOSWorker::readerLoop, this); + logLine("reader_thread=STARTED"); + emit backendStatusChanged(QStringLiteral("Agile"), agileBackendDetails); + return true; +#else + logLine("Fobos SDR Agile dynamic runtime backend is currently implemented for Windows only"); + m_running.store(false); + return false; +#endif +} + + +bool FOBOSWorker::runRegularStart() +{ +#ifdef _WIN32 + logLine("============================================================"); + logLine("SDRangel Fobos SDR regular native source"); + logTimestamp(); + logLine("mode=regular_sync_to_sdrangel_fifo"); + logLine("regular_dll=%s", kRegularDll); + + uint64_t centerFrequency = 0; + int sampleRate = 0; + unsigned int lnaGain = kDefaultLna; + unsigned int vgaGain = kDefaultVga; + int inputMode = kDefaultInputMode; + unsigned int bandwidthPercent = kDefaultBandwidthPercent; + unsigned int gpoMask = kDefaultGpoMask; + bool externalClock = kDefaultExternalClock; + { + QMutexLocker locker(&m_settingsMutex); + centerFrequency = m_centerFrequencyHz; + sampleRate = m_samplerate > 0 ? m_samplerate : kDefaultSampleRateHz; + lnaGain = m_lnaGain; + vgaGain = m_vgaGain; + inputMode = m_inputMode; + bandwidthPercent = m_bandwidthPercent; + gpoMask = m_gpoMask; + externalClock = m_externalClock; + } + + HMODULE lib = nullptr; + { + QMutexLocker sessionLock(&g_sessionMutex); + if (g_deviceBusy) { + logLine("regular_device_busy_guard=ACTIVE another_FOBOS_instance_is_already_streaming"); + m_running.store(false); + return false; + } + if (!g_regularLibrary) { + SetLastError(0); + g_regularLibrary = LoadLibraryA(kRegularDll); + DWORD gle = GetLastError(); + logLine("regular_LoadLibrary=%s handle=0x%p gle=%lu", g_regularLibrary ? "OK" : "FAILED", g_regularLibrary, gle); + if (!g_regularLibrary) { + char errText[512] = {}; + formatLastError(gle, errText, sizeof(errText)); + logLine("regular_LoadLibrary_error='%s'", errText); + m_running.store(false); + return false; + } + } else { + logLine("regular_LoadLibrary=REUSE_PROCESS_SCOPED handle=0x%p", g_regularLibrary); + } + lib = g_regularLibrary; + g_deviceBusy = true; + } + + m_libraryHandle = lib; + m_runtimeBackend = FobosRuntimeBackend::Regular; + + auto getApiInfo = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_api_info")); + auto getDeviceCount = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_device_count")); + auto listDevices = reinterpret_cast(GetProcAddress(lib, "fobos_rx_list_devices")); + auto openDev = reinterpret_cast(GetProcAddress(lib, "fobos_rx_open")); + m_closeDev = reinterpret_cast(GetProcAddress(lib, "fobos_rx_close")); + auto getBoardInfo = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_board_info")); + m_errorName = reinterpret_cast(GetProcAddress(lib, "fobos_rx_error_name")); + m_regularSetFrequency = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_frequency")); + m_regularSetSamplerate = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_samplerate")); + m_regularGetSamplerates = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_samplerates")); + m_regularSetDirectSampling = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_direct_sampling")); + m_regularSetUserGpo = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_user_gpo")); + m_regularSetClkSource = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_clk_source")); + m_regularSetLnaGain = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_lna_gain")); + m_regularSetVgaGain = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_vga_gain")); + auto startSync = reinterpret_cast(GetProcAddress(lib, "fobos_rx_start_sync")); + m_readSync = reinterpret_cast(GetProcAddress(lib, "fobos_rx_read_sync")); + m_stopSync = reinterpret_cast(GetProcAddress(lib, "fobos_rx_stop_sync")); + + const bool ok = getApiInfo && getDeviceCount && listDevices && openDev && m_closeDev && getBoardInfo && m_errorName && + m_regularSetFrequency && m_regularSetSamplerate && m_regularSetLnaGain && m_regularSetVgaGain && + startSync && m_readSync && m_stopSync; + logLine("regular_exports_ok=%s", ok ? "YES" : "NO"); + if (!ok) { + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + m_running.store(false); + return false; + } + + char libVersion[128] = {}; + char drvVersion[128] = {}; + int rInfo = getApiInfo(libVersion, drvVersion); + logLine("regular_api_info_call=result=%d lib='%s' drv='%s'", rInfo, libVersion, drvVersion); + + int count = getDeviceCount(); + logLine("regular_device_count_call=count=%d", count); + if (count <= 0) { + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + m_running.store(false); + return false; + } + + char serials[4096] = {}; + int rList = listDevices(serials); + logLine("regular_list_devices_call=RETURN result=%d serials='%s'", rList, serials); + + fobos_dev_t* dev = nullptr; + int r = openDev(&dev, 0); + logLine("regular_open_call=RETURN result=%d error='%s' dev=0x%p", r, errorName(r), dev); + if ((r != 0) || !dev) { + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + m_running.store(false); + return false; + } + m_dev.store(dev); + + char hw[128] = {}, fw[128] = {}, manufacturer[128] = {}, product[128] = {}, serial[128] = {}; + int rBoard = getBoardInfo(dev, hw, fw, manufacturer, product, serial); + logLine("regular_board_info_call=RETURN result=%d error='%s'", rBoard, errorName(rBoard)); + logLine("regular_board_hw_revision='%s'", hw); + logLine("regular_board_fw_version='%s'", fw); + logLine("regular_board_manufacturer='%s'", manufacturer); + logLine("regular_board_product='%s'", product); + logLine("regular_board_serial='%s'", serial); + const QString regularBackendDetails = QStringLiteral("Fobos regular API %1; driver %2; HW %3; FW %4; serial %5") + .arg(QString::fromLocal8Bit(libVersion)) + .arg(QString::fromLocal8Bit(drvVersion)) + .arg(QString::fromLocal8Bit(hw)) + .arg(QString::fromLocal8Bit(fw)) + .arg(QString::fromLocal8Bit(serial)); + + if (m_regularGetSamplerates) { + double rates[32] = {}; + unsigned int rateCount = 32; + int rRates = m_regularGetSamplerates(dev, rates, &rateCount); + logLine("regular_get_samplerates_call=RETURN result=%d count=%u", rRates, rateCount); + for (unsigned int i = 0; (rRates == 0) && (i < rateCount) && (i < 32); ++i) { + logLine("regular_supported_samplerate[%u]=%.0f", i, rates[i]); + } + } + + r = callSetDirectSampling(dev, inputMode); + logLine("regular_set_direct_sampling_call=RETURN result=%d error='%s' input_mode=%d", r, errorName(r), inputMode); + m_runtimeInputMode.store(inputMode); + r = callSetClockSource(dev, externalClock); + logLine("regular_set_clk_source_call=RETURN result=%d error='%s' external_clock=%s", r, errorName(r), externalClock ? "true" : "false"); + r = callSetGpo(dev, gpoMask); + logLine("regular_set_user_gpo_call=RETURN result=%d error='%s' mask=0x%02X", r, errorName(r), gpoMask); + r = callSetAutoBandwidth(dev, bandwidthPercent); + logLine("regular_set_auto_bandwidth_call=SKIPPED result=%d percent=%u", r, bandwidthPercent); + r = callSetFrequency(dev, static_cast(centerFrequency)); + logLine("regular_set_frequency_call=RETURN result=%d error='%s'", r, errorName(r)); + r = callSetSamplerate(dev, static_cast(sampleRate)); + logLine("regular_set_samplerate_call=RETURN result=%d error='%s'", r, errorName(r)); + r = callSetLnaGain(dev, lnaGain); + logLine("regular_set_lna_gain_call=RETURN result=%d error='%s'", r, errorName(r)); + r = callSetVgaGain(dev, vgaGain); + logLine("regular_set_vga_gain_call=RETURN result=%d error='%s'", r, errorName(r)); + + r = startSync(dev, kSyncComplexBufferLength); + logLine("regular_start_sync_call=RETURN result=%d error='%s' complex_buf_length=%u", r, errorName(r), kSyncComplexBufferLength); + if (r != 0) { + m_dev.store(nullptr); + m_running.store(false); + if (m_closeDev) { + int rc = m_closeDev(dev); + logLine("regular_close_after_start_sync_failure=RETURN result=%d error='%s'", rc, errorName(rc)); + } + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + return false; + } + + { + std::vector startupProbe(kSyncFloatStorage); + bool startupProbeOk = false; + uint32_t startupProbeActual = 0; + int startupProbeResult = -9999; + for (unsigned int attempt = 1; attempt <= kStartupProbeMaxAttempts && !m_stopRequested.load(); ++attempt) { + startupProbeActual = 0; + startupProbeResult = m_readSync(dev, startupProbe.data(), &startupProbeActual); + if ((startupProbeResult == 0) && (startupProbeActual > 0)) { + startupProbeOk = true; + logLine("regular_startup_read_probe=OK attempt=%u actual=%u", attempt, startupProbeActual); + break; + } + logLine("regular_startup_read_probe=RETRY attempt=%u result=%d error='%s' actual=%u", + attempt, startupProbeResult, errorName(startupProbeResult), startupProbeActual); + Sleep(20); + } + if (!startupProbeOk) { + if (m_stopSync) { + int rs = m_stopSync(dev); + logLine("regular_stop_sync_after_startup_probe_failure=RETURN result=%d error='%s'", rs, errorName(rs)); + } + if (m_closeDev) { + int rc = m_closeDev(dev); + logLine("regular_close_after_startup_probe_failure=RETURN result=%d error='%s'", rc, errorName(rc)); + } + m_dev.store(nullptr); + m_syncStarted.store(false); + m_running.store(false); + m_stopRequested.store(false); + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + return false; + } + } + + m_syncStarted.store(true); + m_readerFinished.store(false); + m_readerActive.store(true); + m_stopRequested.store(false); + m_totalReads = 0; + m_totalSamples = 0; + m_totalWritten = 0; + m_failedReads = 0; + + logLine("regular_reader_thread=STARTING"); + m_readerThread = std::thread(&FOBOSWorker::readerLoop, this); + logLine("regular_reader_thread=STARTED"); + emit backendStatusChanged(QStringLiteral("Regular"), regularBackendDetails); + return true; +#else + m_running.store(false); + return false; +#endif +} + +int FOBOSWorker::callSetFrequency(fobos_dev_t* dev, double valueHz) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetFrequency) { + double actual = 0.0; + int r = m_regularSetFrequency(dev, valueHz, &actual); + logLine("regular_frequency_actual_hz=%.0f", actual); + return r; + } + if (m_setFrequency) { + return m_setFrequency(dev, valueHz); + } +#endif + return -9999; +} + +int FOBOSWorker::callSetSamplerate(fobos_dev_t* dev, double valueHz) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetSamplerate) + { + double actual = 0.0; + return m_regularSetSamplerate(dev, valueHz, &actual); + } + + if (m_setSamplerate) + { + return m_setSamplerate(dev, valueHz); + } +#endif + return -9999; +} + +int FOBOSWorker::callSetDirectSampling(fobos_dev_t* dev, int inputMode) +{ +#ifdef _WIN32 + const int direct = (inputMode == 0) ? 0 : 1; + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetDirectSampling) { + return m_regularSetDirectSampling(dev, static_cast(direct)); + } + if (m_setDirectSampling) { + return m_setDirectSampling(dev, direct); + } +#endif + return 0; +} + +int FOBOSWorker::callSetAutoBandwidth(fobos_dev_t* dev, unsigned int bandwidthPercent) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular) { + (void) dev; + (void) bandwidthPercent; + return 0; // regular libfobos has no auto-bandwidth setter + } + if (m_setAutoBandwidth) { + const double ratio = static_cast(bandwidthPercent) * 0.01; + return m_setAutoBandwidth(dev, ratio); + } +#endif + return 0; +} + +int FOBOSWorker::callSetGpo(fobos_dev_t* dev, unsigned int gpoMask) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetUserGpo) { + return m_regularSetUserGpo(dev, static_cast(gpoMask & 0xffu)); + } + if (m_setUserGpo) { + return m_setUserGpo(dev, gpoMask & 0xffu); + } +#endif + return 0; +} + +int FOBOSWorker::callSetClockSource(fobos_dev_t* dev, bool externalClock) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetClkSource) { + return m_regularSetClkSource(dev, externalClock ? 1 : 0); + } + if (m_setClkSource) { + return m_setClkSource(dev, externalClock ? 1 : 0); + } +#endif + return 0; +} + +int FOBOSWorker::callSetLnaGain(fobos_dev_t* dev, unsigned int lnaGain) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetLnaGain) { + return m_regularSetLnaGain(dev, lnaGain); + } + if (m_setLnaGain) { + return m_setLnaGain(dev, lnaGain); + } +#endif + return -9999; +} + +int FOBOSWorker::callSetVgaGain(fobos_dev_t* dev, unsigned int vgaGain) +{ +#ifdef _WIN32 + if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetVgaGain) { + return m_regularSetVgaGain(dev, vgaGain); + } + if (m_setVgaGain) { + return m_setVgaGain(dev, vgaGain); + } +#endif + return -9999; +} + +void FOBOSWorker::readerLoop() +{ +#ifdef _WIN32 + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST); + logLine("reader_thread_priority=THREAD_PRIORITY_HIGHEST"); + fobos_dev_t* dev = m_dev.load(); + if (!dev || !m_readSync) { + logLine("reader_loop=ABORT no_device_or_readSync"); + m_readerActive.store(false); + m_readerFinished.store(true); + return; + } + + std::vector iq(kSyncFloatStorage); + if (m_convertBuffer.size() < kFifoChunkComplexLength) { + m_convertBuffer.resize(kFifoChunkComplexLength); + } + + bool firstLogged = false; + unsigned int startupReadErrors = 0; + uint64_t totalChunks = 0; + uint64_t shortWrites = 0; + uint64_t timingWindowReads = 0; + uint64_t timingWindowChunks = 0; + uint64_t timingWindowSamples = 0; + uint64_t timingWindowWritten = 0; + uint64_t timingWindowShortWrites = 0; + double timingWindowReadMs = 0.0; + double timingWindowConvertPushMs = 0.0; + const auto streamStart = std::chrono::steady_clock::now(); + + logLine("reader_loop=ENTER fast_convert_single_fifo_write=%u", kFifoChunkComplexLength); + + while (!m_stopRequested.load()) { + uint32_t actual = 0; + const auto readStart = std::chrono::steady_clock::now(); + int rr = m_readSync(dev, iq.data(), &actual); + const auto readEnd = std::chrono::steady_clock::now(); + const double readMs = std::chrono::duration(readEnd - readStart).count(); + + if (rr != 0) { + m_failedReads++; + logLine("read_sync_error result=%d error='%s' after_reads=%llu", rr, errorName(rr), static_cast(m_totalReads)); + if (m_stopRequested.load()) { + break; + } + if (!firstLogged) { + ++startupReadErrors; + if (startupReadErrors >= kStartupReaderErrorAbort) { + logLine("startup_read_guard=ABORT no_successful_read_after_errors=%u action=reader_exit_waiting_for_stop", startupReadErrors); + m_stopRequested.store(true); + m_running.store(false); + break; + } + Sleep(20); + } else { + Sleep(2); + } + continue; + } + + if (actual == 0) { + continue; + } + startupReadErrors = 0; + + double mean = 0.0; + double power = 0.0; + double peak = 0.0; + const int inputMode = m_runtimeInputMode.load(); + const double gainScale = m_iqGain.load() * SDR_RX_SCALEF; + const double fixLimit = SDR_RX_SCALEF - 1.0; + const auto convertStart = std::chrono::steady_clock::now(); + + // Fast conversion path: + // Load the software gain once per read block, convert inline, and write the whole + // block to the FIFO once. This keeps the source realtime-safe at high sample rates. + for (uint32_t i = 0; i < actual; ++i) { + float re = iq[2u*i]; + float im = iq[2u*i + 1u]; + + // Direct-sampling mode handling: + // mode 0 RF: normal complex IQ; mode 1 IQ direct: normal complex IQ; + // mode 2 HF1: use I channel as real-only with alternating sign; + // mode 3 HF2: use Q channel as real-only with alternating sign. + if (inputMode == 2) { + if ((i & 1u) != 0u) { re = -re; } + im = 0.0f; + } else if (inputMode == 3) { + re = im; + if ((i & 1u) != 0u) { re = -re; } + im = 0.0f; + } + + if (!firstLogged) { + const double reD = static_cast(re); + const double imD = static_cast(im); + mean += re + im; + power += reD*reD + imD*imD; + peak = std::max(peak, std::max(std::fabs(reD), std::fabs(imD))); + } + + double sr = static_cast(re) * gainScale; + double si = static_cast(im) * gainScale; + if (sr > fixLimit) { sr = fixLimit; } else if (sr < -fixLimit) { sr = -fixLimit; } + if (si > fixLimit) { si = fixLimit; } else if (si < -fixLimit) { si = -fixLimit; } + + // Fast round-to-nearest for normal finite Fobos floats. Fobos Agile API returns finite + // normalized IQ samples; pathological NaN/Inf protection is intentionally not in this + // hot path because it was part of the realtime bottleneck. + m_convertBuffer[i].setReal(static_cast(sr >= 0.0 ? sr + 0.5 : sr - 0.5)); + m_convertBuffer[i].setImag(static_cast(si >= 0.0 ? si + 0.5 : si - 0.5)); + } + + const auto convertEnd = std::chrono::steady_clock::now(); + const unsigned int writtenThisRead = m_sampleFifo->write(m_convertBuffer.cbegin(), m_convertBuffer.cbegin() + actual); + const auto pushEnd = std::chrono::steady_clock::now(); + const unsigned int chunksThisRead = 1; + totalChunks++; + if (writtenThisRead != actual) { + shortWrites++; + timingWindowShortWrites++; + logLine("fifo_write_short requested=%u written=%u fifo_fill=%u", actual, writtenThisRead, m_sampleFifo->fill()); + } + + const double convertMs = std::chrono::duration(convertEnd - convertStart).count(); + const double pushMs = std::chrono::duration(pushEnd - convertEnd).count(); + const double convertPushMs = convertMs + pushMs; + + m_totalReads++; + m_totalSamples += actual; + m_totalWritten += writtenThisRead; + + timingWindowReads++; + timingWindowChunks += chunksThisRead; + timingWindowSamples += actual; + timingWindowWritten += writtenThisRead; + timingWindowReadMs += readMs; + timingWindowConvertPushMs += convertPushMs; + + if (!firstLogged) { + const double denom = static_cast(actual) * 2.0; + logLine("first_read_ok actual=%u written=%u chunks=%u mean=%.9f rms=%.9f peak=%.9f iq_gain=%.3f input_mode=%d read_ms=%.3f convert_ms=%.3f fifo_write_ms=%.3f convert_push_ms=%.3f", + actual, + writtenThisRead, + chunksThisRead, + mean / denom, + std::sqrt(power / denom), + peak, + m_iqGain.load(), + m_runtimeInputMode.load(), + readMs, + convertMs, + pushMs, + convertPushMs); + firstLogged = true; + } + + if ((m_totalReads % 50u) == 0u) { + const auto now = std::chrono::steady_clock::now(); + const double elapsedS = std::chrono::duration(now - streamStart).count(); + const double effectiveMsps = elapsedS > 0.0 ? (static_cast(m_totalSamples) / elapsedS / 1.0e6) : 0.0; + const double avgReadMs = timingWindowReads > 0 ? timingWindowReadMs / static_cast(timingWindowReads) : 0.0; + const double avgConvertPushMs = timingWindowReads > 0 ? timingWindowConvertPushMs / static_cast(timingWindowReads) : 0.0; + logLine("stream_timing reads=%llu samples=%llu written=%llu fifo_writes=%llu fifo_fill=%u avg_read_ms=%.3f avg_convert_write_ms=%.3f effective_msps=%.3f window_short_writes=%llu total_short_writes=%llu", + static_cast(m_totalReads), + static_cast(m_totalSamples), + static_cast(m_totalWritten), + static_cast(totalChunks), + m_sampleFifo->fill(), + avgReadMs, + avgConvertPushMs, + effectiveMsps, + static_cast(timingWindowShortWrites), + static_cast(shortWrites)); + timingWindowReads = 0; + timingWindowChunks = 0; + timingWindowSamples = 0; + timingWindowWritten = 0; + timingWindowShortWrites = 0; + timingWindowReadMs = 0.0; + timingWindowConvertPushMs = 0.0; + } + } + + logLine("stream_loop_exit reads=%llu samples=%llu written=%llu failed_reads=%llu fifo_fill=%u", + static_cast(m_totalReads), + static_cast(m_totalSamples), + static_cast(m_totalWritten), + static_cast(m_failedReads), + m_sampleFifo->fill()); + + m_readerActive.store(false); + m_readerFinished.store(true); +#endif +} + +void FOBOSWorker::cleanupAfterReaderJoined(bool readerJoined) +{ +#ifdef _WIN32 + fobos_dev_t* dev = m_dev.load(); + + if (!dev) { + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + logLine("cleanup=no_device"); + return; + } + + if (readerJoined && m_closeDev) { + Sleep(100); + logLine("close_call=START_AFTER_READER_JOIN"); + int r = m_closeDev(dev); + logLine("close_call=RETURN result=%d error='%s'", r, errorName(r)); + } else { + logLine("close_call=SKIPPED_READER_NOT_JOINED"); + } + + m_dev.store(nullptr); + m_running.store(false); + m_stopRequested.store(false); + + { + QMutexLocker sessionLock(&g_sessionMutex); + g_deviceBusy = false; + } + + logLine("freelibrary_call=SKIPPED_PROCESS_SCOPED_DLL"); + logLine("source_result=STOPPED_CLEANLY_READER_JOINED_CLOSE_DONE"); +#endif +} + +void FOBOSWorker::logLine(const char* fmt, ...) const +{ +#if defined(FOBOS_DEBUG_FILE_LOG) + FILE* f = nullptr; + fopen_s(&f, kLogPath, "a"); + if (!f) { + return; + } + va_list ap; + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + fputc('\n', f); + fclose(f); +#else + (void) fmt; +#endif +} + +void FOBOSWorker::logTimestamp() const +{ +#if defined(FOBOS_DEBUG_FILE_LOG) + const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); + logLine("timestamp=%s", ts.toUtf8().constData()); +#endif +} + +const char* FOBOSWorker::errorName(int code) const +{ +#ifdef _WIN32 + if (m_errorName) { + const char* s = m_errorName(code); + return s ? s : "null_error_name"; + } +#endif + return "error_name_unavailable"; +} + +FixReal FOBOSWorker::floatToFix(float v) const +{ + // Legacy helper kept for ABI/source compatibility. The realtime reader loop uses an + // inline fast conversion path and does not call this function per sample. + if (!std::isfinite(v)) { + return 0; + } + + const double limit = SDR_RX_SCALEF - 1.0; + double scaled = static_cast(v) * m_iqGain.load() * SDR_RX_SCALEF; + + if (scaled > limit) { + scaled = limit; + } else if (scaled < -limit) { + scaled = -limit; + } + + return static_cast(scaled >= 0.0 ? scaled + 0.5 : scaled - 0.5); +} diff --git a/plugins/samplesource/fobos/fobosworker.h b/plugins/samplesource/fobos/fobosworker.h new file mode 100644 index 000000000..fbedc1329 --- /dev/null +++ b/plugins/samplesource/fobos/fobosworker.h @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////////////////////// +// SDRangel Fobos SDR Agile native source backend +// Agile-first synchronous streaming worker. +// Stop lifecycle: request reader exit, join reader, then stop_sync and close the device. +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef _FOBOS_FOBOSWORKER_H_ +#define _FOBOS_FOBOSWORKER_H_ + +#include +#include +#include +#include +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "fobossettings.h" + +struct fobos_dev_t; + +enum class FobosRuntimeBackend { None, Agile, Regular }; + +class FOBOSWorker : public QObject +{ + Q_OBJECT +public: + explicit FOBOSWorker(SampleSinkFifo* sampleFifo, QObject* parent = nullptr); + ~FOBOSWorker() override; + + void startWork(); + void stopWork(); + + // Kept for compatibility with the TestSource-derived input/controller code. + void setSamplerate(int samplerate); + void setCenterFrequency(uint64_t centerFrequencyHz); + void setLog2Decimation(unsigned int log2_decim); + void setFcPos(int fcPos); + void setBitSize(uint32_t bitSizeIndex); + void setAmplitudeBits(int32_t amplitudeBits); + void setLnaGain(unsigned int lnaGain); + void setVgaGain(unsigned int vgaGain); + void setInputMode(int inputMode); + void setBandwidthPercent(unsigned int bandwidthPercent); + void setGpoMask(unsigned int gpoMask); + void setExternalClock(bool externalClock); + void setDCFactor(float dcFactor); + void setIFactor(float iFactor); + void setQFactor(float qFactor); + void setPhaseImbalance(float phaseImbalance); + void setFrequencyShift(int shift); + void setToneFrequency(int toneFrequency); + void setModulation(int modulation); + void setAMModulation(float amModulation); + void setFMDeviation(float deviation); + void setPattern0(); + void setPattern1(); + void setPattern2(); + +signals: + void backendStatusChanged(const QString& backend, const QString& details); + +private: +#ifdef _WIN32 + using fobos_sdr_get_api_info_t = int (__cdecl *)(char* lib_version, char* drv_version); + using fobos_sdr_get_device_count_t = int (__cdecl *)(); + using fobos_sdr_list_devices_t = int (__cdecl *)(char* serials); + using fobos_sdr_open_t = int (__cdecl *)(fobos_dev_t** out_dev, uint32_t index); + using fobos_sdr_close_t = int (__cdecl *)(fobos_dev_t* dev); + using fobos_sdr_get_board_info_t = int (__cdecl *)(fobos_dev_t* dev, char* hw_revision, char* fw_version, char* manufacturer, char* product, char* serial); + using fobos_sdr_error_name_t = const char* (__cdecl *)(int error_code); + using fobos_sdr_set_frequency_t = int (__cdecl *)(fobos_dev_t* dev, double value_hz); + using fobos_sdr_set_samplerate_t = int (__cdecl *)(fobos_dev_t* dev, double value_hz); + using fobos_sdr_get_samplerates_t = int (__cdecl *)(fobos_dev_t* dev, double* values, uint32_t* count); + using fobos_sdr_set_direct_sampling_t = int (__cdecl *)(fobos_dev_t* dev, int value); + using fobos_sdr_set_auto_bandwidth_t = int (__cdecl *)(fobos_dev_t* dev, double value); + using fobos_sdr_set_user_gpo_t = int (__cdecl *)(fobos_dev_t* dev, uint32_t value); + using fobos_sdr_set_clk_source_t = int (__cdecl *)(fobos_dev_t* dev, int value); + using fobos_sdr_set_lna_gain_t = int (__cdecl *)(fobos_dev_t* dev, unsigned int value); + using fobos_sdr_set_vga_gain_t = int (__cdecl *)(fobos_dev_t* dev, unsigned int value); + using fobos_sdr_start_sync_t = int (__cdecl *)(fobos_dev_t* dev, uint32_t buf_length); + using fobos_sdr_read_sync_t = int (__cdecl *)(fobos_dev_t* dev, float* buf, uint32_t* actual_buf_length); + using fobos_sdr_stop_sync_t = int (__cdecl *)(fobos_dev_t* dev); + + using fobos_rx_get_api_info_t = int (__cdecl *)(char* lib_version, char* drv_version); + using fobos_rx_get_device_count_t = int (__cdecl *)(); + using fobos_rx_list_devices_t = int (__cdecl *)(char* serials); + using fobos_rx_open_t = int (__cdecl *)(fobos_dev_t** out_dev, uint32_t index); + using fobos_rx_close_t = int (__cdecl *)(fobos_dev_t* dev); + using fobos_rx_get_board_info_t = int (__cdecl *)(fobos_dev_t* dev, char* hw_revision, char* fw_version, char* manufacturer, char* product, char* serial); + using fobos_rx_error_name_t = const char* (__cdecl *)(int error_code); + using fobos_rx_set_frequency_t = int (__cdecl *)(fobos_dev_t* dev, double value_hz, double* actual_hz); + using fobos_rx_set_samplerate_t = int (__cdecl *)(fobos_dev_t* dev, double value_hz, double* actual_hz); + using fobos_rx_get_samplerates_t = int (__cdecl *)(fobos_dev_t* dev, double* values, unsigned int* count); + using fobos_rx_set_direct_sampling_t = int (__cdecl *)(fobos_dev_t* dev, unsigned int enabled); + using fobos_rx_set_user_gpo_t = int (__cdecl *)(fobos_dev_t* dev, uint8_t value); + using fobos_rx_set_clk_source_t = int (__cdecl *)(fobos_dev_t* dev, int value); + using fobos_rx_set_lna_gain_t = int (__cdecl *)(fobos_dev_t* dev, unsigned int value); + using fobos_rx_set_vga_gain_t = int (__cdecl *)(fobos_dev_t* dev, unsigned int value); + using fobos_rx_start_sync_t = int (__cdecl *)(fobos_dev_t* dev, uint32_t buf_length); + using fobos_rx_read_sync_t = int (__cdecl *)(fobos_dev_t* dev, float* buf, uint32_t* actual_buf_length); + using fobos_rx_stop_sync_t = int (__cdecl *)(fobos_dev_t* dev); +#endif + + bool runAgileStart(); + bool runRegularStart(); + int callSetFrequency(fobos_dev_t* dev, double valueHz); + int callSetSamplerate(fobos_dev_t* dev, double valueHz); + int callSetDirectSampling(fobos_dev_t* dev, int inputMode); + int callSetAutoBandwidth(fobos_dev_t* dev, unsigned int bandwidthPercent); + int callSetGpo(fobos_dev_t* dev, unsigned int gpoMask); + int callSetClockSource(fobos_dev_t* dev, bool externalClock); + int callSetLnaGain(fobos_dev_t* dev, unsigned int lnaGain); + int callSetVgaGain(fobos_dev_t* dev, unsigned int vgaGain); + void readerLoop(); + void cleanupAfterReaderJoined(bool readerJoined); + void logLine(const char* fmt, ...) const; + void logTimestamp() const; + const char* errorName(int code) const; + FixReal floatToFix(float v) const; + + SampleSinkFifo* m_sampleFifo; + SampleVector m_convertBuffer; + + QMutex m_settingsMutex; + uint64_t m_centerFrequencyHz; + int m_samplerate; + unsigned int m_log2Decim; + int m_fcPos; + int m_frequencyShift; + unsigned int m_lnaGain; + unsigned int m_vgaGain; + int m_inputMode; + unsigned int m_bandwidthPercent; + unsigned int m_gpoMask; + bool m_externalClock; + std::atomic m_iqGain; + std::atomic m_runtimeInputMode; + + QMutex m_gainDiagMutex; + bool m_gainDiagActive; + bool m_gainDiagCollecting; + QString m_gainDiagKind; + unsigned int m_gainDiagValue; + unsigned int m_gainDiagSkipBuffers; + unsigned int m_gainDiagTargetBuffers; + unsigned int m_gainDiagCollectedBuffers; + uint64_t m_gainDiagSamples; + double m_gainDiagPower; + double m_gainDiagPeak; + + std::atomic_bool m_running; + std::atomic_bool m_stopRequested; + std::atomic_bool m_syncStarted; + std::atomic_bool m_readerActive; + std::atomic_bool m_readerFinished; + std::atomic m_dev; + std::thread m_readerThread; + FobosRuntimeBackend m_runtimeBackend; + +#ifdef _WIN32 + void* m_libraryHandle; + fobos_sdr_error_name_t m_errorName; + fobos_sdr_close_t m_closeDev; + fobos_sdr_read_sync_t m_readSync; + fobos_sdr_stop_sync_t m_stopSync; + fobos_sdr_set_frequency_t m_setFrequency; + fobos_sdr_set_samplerate_t m_setSamplerate; + fobos_sdr_get_samplerates_t m_getSamplerates; + fobos_sdr_set_direct_sampling_t m_setDirectSampling; + fobos_sdr_set_auto_bandwidth_t m_setAutoBandwidth; + fobos_sdr_set_user_gpo_t m_setUserGpo; + fobos_sdr_set_clk_source_t m_setClkSource; + fobos_sdr_set_lna_gain_t m_setLnaGain; + fobos_sdr_set_vga_gain_t m_setVgaGain; + + fobos_rx_set_frequency_t m_regularSetFrequency; + fobos_rx_set_samplerate_t m_regularSetSamplerate; + fobos_rx_get_samplerates_t m_regularGetSamplerates; + fobos_rx_set_direct_sampling_t m_regularSetDirectSampling; + fobos_rx_set_user_gpo_t m_regularSetUserGpo; + fobos_rx_set_clk_source_t m_regularSetClkSource; + fobos_rx_set_lna_gain_t m_regularSetLnaGain; + fobos_rx_set_vga_gain_t m_regularSetVgaGain; +#endif + + uint64_t m_totalReads; + uint64_t m_totalSamples; + uint64_t m_totalWritten; + uint64_t m_failedReads; +}; + +#endif // _FOBOS_FOBOSWORKER_H_ diff --git a/plugins/samplesource/fobos/readme.md b/plugins/samplesource/fobos/readme.md new file mode 100644 index 000000000..6bd8f3ae3 --- /dev/null +++ b/plugins/samplesource/fobos/readme.md @@ -0,0 +1,45 @@ +# Fobos SDR input plugin + +This plugin adds native SDRangel sample source support for RigExpert Fobos SDR devices. + +It supports automatic backend selection: + +- Agile firmware/API through `fobos_sdr.dll` +- regular/classic firmware/API through `fobos.dll` + +Initial scope: + +- Fobos SDR backend loaded at runtime: Agile `fobos_sdr.dll` first, regular `fobos.dll` fallback +- Device enumeration and open/close +- Center frequency control +- Sample rate selection +- Relative bandwidth control for Agile backend (shown but ignored by Regular backend) +- LNA gain control, 0..2 +- VGA gain control, 0..31 +- GPO control +- Internal/external clock selection +- Synchronous streaming into the SDRangel sample FIFO + +Runtime notes: + +- On Windows, place `fobos_sdr.dll` and its runtime dependencies next to the SDRangel executable or make them available through `PATH`. +- The plugin intentionally does not use developer-machine paths such as `C:\dev\...`. +- File logging is disabled by default. Configure with `-DFOBOS_DEBUG_FILE_LOG=ON` to write `sdrangel_fobos_source.log` for local diagnostics. + +The plugin is intended as a native SDRangel sample source. It does not include analog video decoding or SDR# compatibility code. + +Initial integration and hardware testing: Alex Antonov UT2UM Kyiv 2026. + +## Windows runtime packaging + +On Windows, official SDRangel builds are expected to provide the Fobos SDR runtime packages through the SDRangel Windows dependency repository: + +- `external/windows/fobos-sdr` for Agile firmware/API +- `external/windows/fobos-regular` for regular/classic firmware/API + +The plugin build copies `fobos_sdr.dll`, `fobos.dll`, and the required `libusb-1.0.dll` runtime to the SDRangel binary directory so users do not need to copy runtime DLLs manually. + +The companion dependency repository updates are tracked separately as: + +- `sdrangel-windows-libraries` PR #32 for Agile runtime +- `sdrangel-windows-libraries` PR #33 for regular/classic runtime