From 67d948d440317c89065549c8dcfe4cb3ebfc0265 Mon Sep 17 00:00:00 2001 From: f4exb Date: Thu, 1 Feb 2018 09:12:38 +0100 Subject: [PATCH] AirspyHF (float): new plugin structure --- plugins/samplesource/CMakeLists.txt | 1 + plugins/samplesource/airspyhff/CMakeLists.txt | 78 +++ .../airspy.pro => airspyhff/airspyhff.pro} | 24 +- .../samplesource/airspyhff/airspyhffgui.cpp | 398 ++++++++++++++ plugins/samplesource/airspyhff/airspyhffgui.h | 94 ++++ .../samplesource/airspyhff/airspyhffgui.ui | 448 ++++++++++++++++ .../samplesource/airspyhff/airspyhffinput.cpp | 492 ++++++++++++++++++ .../samplesource/airspyhff/airspyhffinput.h | 147 ++++++ .../airspyhff/airspyhffplugin.cpp | 126 +++++ .../samplesource/airspyhff/airspyhffplugin.h | 53 ++ .../airspyhff/airspyhffsettings.cpp | 83 +++ .../airspyhff/airspyhffsettings.h | 36 ++ .../airspyhff/airspyhffthread.cpp | 142 +++++ .../samplesource/airspyhff/airspyhffthread.h | 67 +++ plugins/samplesource/airspyhff/readme.md | 105 ++++ 15 files changed, 2282 insertions(+), 12 deletions(-) create mode 100644 plugins/samplesource/airspyhff/CMakeLists.txt rename plugins/samplesource/{airspyhf/airspy.pro => airspyhff/airspyhff.pro} (78%) create mode 100644 plugins/samplesource/airspyhff/airspyhffgui.cpp create mode 100644 plugins/samplesource/airspyhff/airspyhffgui.h create mode 100644 plugins/samplesource/airspyhff/airspyhffgui.ui create mode 100644 plugins/samplesource/airspyhff/airspyhffinput.cpp create mode 100644 plugins/samplesource/airspyhff/airspyhffinput.h create mode 100644 plugins/samplesource/airspyhff/airspyhffplugin.cpp create mode 100644 plugins/samplesource/airspyhff/airspyhffplugin.h create mode 100644 plugins/samplesource/airspyhff/airspyhffsettings.cpp create mode 100644 plugins/samplesource/airspyhff/airspyhffsettings.h create mode 100644 plugins/samplesource/airspyhff/airspyhffthread.cpp create mode 100644 plugins/samplesource/airspyhff/airspyhffthread.h create mode 100644 plugins/samplesource/airspyhff/readme.md diff --git a/plugins/samplesource/CMakeLists.txt b/plugins/samplesource/CMakeLists.txt index 106f7c903..56056e57a 100644 --- a/plugins/samplesource/CMakeLists.txt +++ b/plugins/samplesource/CMakeLists.txt @@ -40,6 +40,7 @@ endif(LIBUSB_FOUND AND LIBAIRSPY_FOUND) find_package(LibAIRSPYHF) if(LIBUSB_FOUND AND LIBAIRSPYHF_FOUND) add_subdirectory(airspyhf) + add_subdirectory(airspyhff) endif(LIBUSB_FOUND AND LIBAIRSPYHF_FOUND) find_package(LibHACKRF) diff --git a/plugins/samplesource/airspyhff/CMakeLists.txt b/plugins/samplesource/airspyhff/CMakeLists.txt new file mode 100644 index 000000000..9ce86ec97 --- /dev/null +++ b/plugins/samplesource/airspyhff/CMakeLists.txt @@ -0,0 +1,78 @@ +project(airspyhff) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + +set(airspyhff_SOURCES + airspyhffgui.cpp + airspyhffinput.cpp + airspyhffplugin.cpp + airspyhffsettings.cpp + airspyhffthread.cpp +) + +set(airspyhff_HEADERS + airspyhffgui.h + airspyhffinput.h + airspyhffplugin.h + airspyhffsettings.h + airspyhffthread.h +) + +set(airspyhff_FORMS + airspyhffgui.ui +) + +if (BUILD_DEBIAN) +include_directories( + . + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${LIBAIRSPYHFSRC} + ${LIBAIRSPYHFSRC}/libairspyhf/src +) +else (BUILD_DEBIAN) +include_directories( + . + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${LIBAIRSPYHF_INCLUDE_DIR} +) +endif (BUILD_DEBIAN) + +#include(${QT_USE_FILE}) +#add_definitions(${QT_DEFINITIONS}) +add_definitions("${QT_DEFINITIONS} -DLIBAIRSPY_DYN_RATES") +add_definitions(-DQT_PLUGIN) +add_definitions(-DQT_SHARED) + +#qt4_wrap_cpp(airspyhff_HEADERS_MOC ${airspyhff_HEADERS}) +qt5_wrap_ui(airspyhff_FORMS_HEADERS ${airspyhff_FORMS}) + +add_library(inputairspyhff SHARED + ${airspyhff_SOURCES} + ${airspyhff_HEADERS_MOC} + ${airspyhff_FORMS_HEADERS} +) + +if (BUILD_DEBIAN) +target_link_libraries(inputairspyhff + ${QT_LIBRARIES} + airspyhff + sdrbase + sdrgui + swagger +) +else (BUILD_DEBIAN) +target_link_libraries(inputairspyhff + ${QT_LIBRARIES} + ${LIBAIRSPYHF_LIBRARIES} + sdrbase + sdrgui + swagger +) +endif (BUILD_DEBIAN) + + +qt5_use_modules(inputairspyhff Core Widgets) + +install(TARGETS inputairspyhff DESTINATION lib/plugins/samplesource) diff --git a/plugins/samplesource/airspyhf/airspy.pro b/plugins/samplesource/airspyhff/airspyhff.pro similarity index 78% rename from plugins/samplesource/airspyhf/airspy.pro rename to plugins/samplesource/airspyhff/airspyhff.pro index c31ae2e71..c16537701 100644 --- a/plugins/samplesource/airspyhf/airspy.pro +++ b/plugins/samplesource/airspyhff/airspyhff.pro @@ -9,7 +9,7 @@ CONFIG += plugin QT += core gui widgets multimedia opengl -TARGET = inputairspyhf +TARGET = inputairspyhff CONFIG(MINGW32):LIBAIRSPYHFSRC = "D:\softs\airspyhf" CONFIG(MINGW64):LIBAIRSPYHFSRC = "D:\softs\airspyhf" @@ -28,19 +28,19 @@ QMAKE_CXXFLAGS += -std=c++11 CONFIG(Release):build_subdir = release CONFIG(Debug):build_subdir = debug -SOURCES += airspyhfgui.cpp\ - airspyhfinput.cpp\ - airspyhfplugin.cpp\ - airspyhfsettings.cpp\ - airspyhfthread.cpp +SOURCES += airspyhffgui.cpp\ + airspyhffinput.cpp\ + airspyhffplugin.cpp\ + airspyhffsettings.cpp\ + airspyhffthread.cpp -HEADERS += airspyhfgui.h\ - airspyhfinput.h\ - airspyhfplugin.h\ - airspyhfsettings.h\ - airspyhfthread.h +HEADERS += airspyhffgui.h\ + airspyhffinput.h\ + airspyhffplugin.h\ + airspyhffsettings.h\ + airspyhffthread.h -FORMS += airspyhfgui.ui +FORMS += airspyhffgui.ui LIBS += -L../../../sdrbase/$${build_subdir} -lsdrbase LIBS += -L../../../sdrgui/$${build_subdir} -lsdrgui diff --git a/plugins/samplesource/airspyhff/airspyhffgui.cpp b/plugins/samplesource/airspyhff/airspyhffgui.cpp new file mode 100644 index 000000000..333ab41e0 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffgui.cpp @@ -0,0 +1,398 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include + +#include +#include "device/deviceuiset.h" +#include + +#include "ui_airspyhffgui.h" +#include "gui/colormapper.h" +#include "gui/glspectrum.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "airspyhffgui.h" + +AirspyHFFGui::AirspyHFFGui(DeviceUISet *deviceUISet, QWidget* parent) : + QWidget(parent), + ui(new Ui::AirspyHFFGui), + m_deviceUISet(deviceUISet), + m_doApplySettings(true), + m_forceSettings(true), + m_settings(), + m_sampleSource(0), + m_lastEngineState((DSPDeviceSourceEngine::State)-1) +{ + m_sampleSource = (AirspyHFFInput*) m_deviceUISet->m_deviceSourceAPI->getSampleSource(); + + ui->setupUi(this); + ui->centerFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + updateFrequencyLimits(); + + connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateHardware())); + connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); + m_statusTimer.start(500); + + displaySettings(); + + m_rates = ((AirspyHFFInput*) m_sampleSource)->getSampleRates(); + displaySampleRates(); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()), Qt::QueuedConnection); + + sendSettings(); +} + +AirspyHFFGui::~AirspyHFFGui() +{ + delete ui; +} + +void AirspyHFFGui::destroy() +{ + delete this; +} + +void AirspyHFFGui::setName(const QString& name) +{ + setObjectName(name); +} + +QString AirspyHFFGui::getName() const +{ + return objectName(); +} + +void AirspyHFFGui::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + sendSettings(); +} + +qint64 AirspyHFFGui::getCenterFrequency() const +{ + return m_settings.m_centerFrequency; +} + +void AirspyHFFGui::setCenterFrequency(qint64 centerFrequency) +{ + m_settings.m_centerFrequency = centerFrequency; + displaySettings(); + sendSettings(); +} + +QByteArray AirspyHFFGui::serialize() const +{ + return m_settings.serialize(); +} + +bool AirspyHFFGui::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + m_forceSettings = true; + sendSettings(); + return true; + } else { + resetToDefaults(); + return false; + } +} + +bool AirspyHFFGui::handleMessage(const Message& message) +{ + if (AirspyHFFInput::MsgConfigureAirspyHF::match(message)) + { + const AirspyHFFInput::MsgConfigureAirspyHF& cfg = (AirspyHFFInput::MsgConfigureAirspyHF&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (AirspyHFFInput::MsgStartStop::match(message)) + { + AirspyHFFInput::MsgStartStop& notif = (AirspyHFFInput::MsgStartStop&) message; + blockApplySettings(true); + ui->startStop->setChecked(notif.getStartStop()); + blockApplySettings(false); + + return true; + } + else + { + return false; + } +} + +void AirspyHFFGui::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != 0) + { + qDebug("AirspyHFGui::handleInputMessages: message: %s", message->getIdentifier()); + + if (DSPSignalNotification::match(*message)) + { + DSPSignalNotification* notif = (DSPSignalNotification*) message; + m_sampleRate = notif->getSampleRate(); + m_deviceCenterFrequency = notif->getCenterFrequency(); + qDebug("AirspyGui::handleInputMessages: DSPSignalNotification: SampleRate:%d, CenterFrequency:%llu", notif->getSampleRate(), notif->getCenterFrequency()); + updateSampleRateAndFrequency(); + + delete message; + } + else + { + if (handleMessage(*message)) + { + delete message; + } + } + } +} + +void AirspyHFFGui::updateSampleRateAndFrequency() +{ + m_deviceUISet->getSpectrum()->setSampleRate(m_sampleRate); + m_deviceUISet->getSpectrum()->setCenterFrequency(m_deviceCenterFrequency); + ui->deviceRateText->setText(tr("%1k").arg((float)m_sampleRate / 1000)); +} + +void AirspyHFFGui::updateFrequencyLimits() +{ + // values in kHz + qint64 deltaFrequency = m_settings.m_transverterMode ? m_settings.m_transverterDeltaFrequency/1000 : 0; + + qint64 minLimit; + qint64 maxLimit; + + switch(m_settings.m_bandIndex) + { + case 1: + minLimit = AirspyHFFInput::loLowLimitFreqVHF/1000 + deltaFrequency; + maxLimit = AirspyHFFInput::loHighLimitFreqVHF/1000 + deltaFrequency; + break; + case 0: + default: + minLimit = AirspyHFFInput::loLowLimitFreqHF/1000 + deltaFrequency; + maxLimit = AirspyHFFInput::loHighLimitFreqHF/1000 + deltaFrequency; + break; + } + + minLimit = minLimit < 0 ? 0 : minLimit > 9999999 ? 9999999 : minLimit; + maxLimit = maxLimit < 0 ? 0 : maxLimit > 9999999 ? 9999999 : maxLimit; + + qDebug("AirspyHFGui::updateFrequencyLimits: delta: %lld min: %lld max: %lld", deltaFrequency, minLimit, maxLimit); + + ui->centerFrequency->setValueRange(7, minLimit, maxLimit); +} + +void AirspyHFFGui::displaySettings() +{ + blockApplySettings(true); + ui->band->blockSignals(true); + m_settings.m_bandIndex = m_settings.m_centerFrequency <= 31000000UL ? 0 : 1; // override + ui->band->setCurrentIndex(m_settings.m_bandIndex); + updateFrequencyLimits(); + ui->transverter->setDeltaFrequency(m_settings.m_transverterDeltaFrequency); + ui->transverter->setDeltaFrequencyActive(m_settings.m_transverterMode); + ui->centerFrequency->setValue(m_settings.m_centerFrequency / 1000); + ui->LOppm->setValue(m_settings.m_LOppmTenths); + ui->LOppmText->setText(QString("%1").arg(QString::number(m_settings.m_LOppmTenths/10.0, 'f', 1))); + ui->sampleRate->setCurrentIndex(m_settings.m_devSampleRateIndex); + ui->decim->setCurrentIndex(m_settings.m_log2Decim); + ui->band->blockSignals(false); + blockApplySettings(false); +} + +void AirspyHFFGui::displaySampleRates() +{ + unsigned int savedIndex = m_settings.m_devSampleRateIndex; + ui->sampleRate->blockSignals(true); + + if (m_rates.size() > 0) + { + ui->sampleRate->clear(); + + for (unsigned int i = 0; i < m_rates.size(); i++) + { + int sampleRate = m_rates[i]/1000; + ui->sampleRate->addItem(QString("%1").arg(QString("%1").arg(sampleRate, 5, 10, QChar(' ')))); + } + } + + ui->sampleRate->blockSignals(false); + + if (savedIndex < m_rates.size()) + { + ui->sampleRate->setCurrentIndex(savedIndex); + } + else + { + ui->sampleRate->setCurrentIndex((int) m_rates.size()-1); + } +} + +void AirspyHFFGui::sendSettings() +{ + if(!m_updateTimer.isActive()) + m_updateTimer.start(100); +} + +void AirspyHFFGui::on_centerFrequency_changed(quint64 value) +{ + m_settings.m_centerFrequency = value * 1000; + sendSettings(); +} + +void AirspyHFFGui::on_LOppm_valueChanged(int value) +{ + m_settings.m_LOppmTenths = value; + ui->LOppmText->setText(QString("%1").arg(QString::number(m_settings.m_LOppmTenths/10.0, 'f', 1))); + sendSettings(); +} + +void AirspyHFFGui::on_resetLOppm_clicked() +{ + ui->LOppm->setValue(0); +} + +void AirspyHFFGui::on_sampleRate_currentIndexChanged(int index) +{ + m_settings.m_devSampleRateIndex = index; + sendSettings(); +} + +void AirspyHFFGui::on_decim_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 5)) + return; + m_settings.m_log2Decim = index; + sendSettings(); +} + +void AirspyHFFGui::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + AirspyHFFInput::MsgStartStop *message = AirspyHFFInput::MsgStartStop::create(checked); + m_sampleSource->getInputMessageQueue()->push(message); + } +} + +void AirspyHFFGui::on_record_toggled(bool checked) +{ + if (checked) { + ui->record->setStyleSheet("QToolButton { background-color : red; }"); + } else { + ui->record->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + } + + AirspyHFFInput::MsgFileRecord* message = AirspyHFFInput::MsgFileRecord::create(checked); + m_sampleSource->getInputMessageQueue()->push(message); +} + +void AirspyHFFGui::on_transverter_clicked() +{ + m_settings.m_transverterMode = ui->transverter->getDeltaFrequencyAcive(); + m_settings.m_transverterDeltaFrequency = ui->transverter->getDeltaFrequency(); + qDebug("AirspyHFGui::on_transverter_clicked: %lld Hz %s", m_settings.m_transverterDeltaFrequency, m_settings.m_transverterMode ? "on" : "off"); + updateFrequencyLimits(); + m_settings.m_centerFrequency = ui->centerFrequency->getValueNew()*1000; + sendSettings(); +} + +void AirspyHFFGui::on_band_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 1)) { + return; + } + + m_settings.m_bandIndex = index; + updateFrequencyLimits(); + qDebug("AirspyHFGui::on_band_currentIndexChanged: freq: %llu", ui->centerFrequency->getValueNew() * 1000); + m_settings.m_centerFrequency = ui->centerFrequency->getValueNew() * 1000; + sendSettings(); +} + +void AirspyHFFGui::updateHardware() +{ + qDebug() << "AirspyHFGui::updateHardware"; + AirspyHFFInput::MsgConfigureAirspyHF* message = AirspyHFFInput::MsgConfigureAirspyHF::create(m_settings, m_forceSettings); + m_sampleSource->getInputMessageQueue()->push(message); + m_forceSettings = false; + m_updateTimer.stop(); +} + +void AirspyHFFGui::updateStatus() +{ + int state = m_deviceUISet->m_deviceSourceAPI->state(); + + if(m_lastEngineState != state) + { + switch(state) + { + case DSPDeviceSourceEngine::StNotStarted: + ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case DSPDeviceSourceEngine::StIdle: + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"); + break; + case DSPDeviceSourceEngine::StRunning: + ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + break; + case DSPDeviceSourceEngine::StError: + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + QMessageBox::information(this, tr("Message"), m_deviceUISet->m_deviceSourceAPI->errorMessage()); + break; + default: + break; + } + + m_lastEngineState = state; + } +} + +uint32_t AirspyHFFGui::getDevSampleRate(unsigned int rate_index) +{ + if (rate_index < m_rates.size()) + { + return m_rates[rate_index]; + } + else + { + return m_rates[0]; + } +} + +int AirspyHFFGui::getDevSampleRateIndex(uint32_t sampeRate) +{ + for (unsigned int i=0; i < m_rates.size(); i++) + { + if (sampeRate == m_rates[i]) + { + return i; + } + } + + return -1; +} diff --git a/plugins/samplesource/airspyhff/airspyhffgui.h b/plugins/samplesource/airspyhff/airspyhffgui.h new file mode 100644 index 000000000..8815d5e5c --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffgui.h @@ -0,0 +1,94 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AIRSPYHFFGUI_H +#define INCLUDE_AIRSPYHFFGUI_H + +#include +#include +#include + +#include "util/messagequeue.h" + +#include "airspyhffinput.h" + +class DeviceUISet; + +namespace Ui { + class AirspyHFFGui; + class AirspyHFFSampleRates; +} + +class AirspyHFFGui : public QWidget, public PluginInstanceGUI { + Q_OBJECT + +public: + explicit AirspyHFFGui(DeviceUISet *deviceUISet, QWidget* parent = 0); + virtual ~AirspyHFFGui(); + virtual void destroy(); + + void setName(const QString& name); + QString getName() const; + + void resetToDefaults(); + virtual qint64 getCenterFrequency() const; + virtual void setCenterFrequency(qint64 centerFrequency); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue* getInputMessageQueue() { return &m_inputMessageQueue; } + virtual bool handleMessage(const Message& message); + uint32_t getDevSampleRate(unsigned int index); + int getDevSampleRateIndex(uint32_t sampleRate); + +private: + Ui::AirspyHFFGui* ui; + + DeviceUISet* m_deviceUISet; + bool m_doApplySettings; + bool m_forceSettings; + AirspyHFFSettings m_settings; + QTimer m_updateTimer; + QTimer m_statusTimer; + std::vector m_rates; + DeviceSampleSource* m_sampleSource; + int m_sampleRate; + quint64 m_deviceCenterFrequency; //!< Center frequency in device + int m_lastEngineState; + MessageQueue m_inputMessageQueue; + + void blockApplySettings(bool block) { m_doApplySettings = !block; } + void displaySettings(); + void displaySampleRates(); + void sendSettings(); + void updateSampleRateAndFrequency(); + void updateFrequencyLimits(); + +private slots: + void on_centerFrequency_changed(quint64 value); + void on_LOppm_valueChanged(int value); + void on_resetLOppm_clicked(); + void on_sampleRate_currentIndexChanged(int index); + void on_decim_currentIndexChanged(int index); + void on_startStop_toggled(bool checked); + void on_record_toggled(bool checked); + void on_transverter_clicked(); + void on_band_currentIndexChanged(int index); + void updateHardware(); + void updateStatus(); + void handleInputMessages(); +}; + +#endif // INCLUDE_AIRSPYHFGUI_H diff --git a/plugins/samplesource/airspyhff/airspyhffgui.ui b/plugins/samplesource/airspyhff/airspyhffgui.ui new file mode 100644 index 000000000..218765a3c --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffgui.ui @@ -0,0 +1,448 @@ + + + AirspyHFFGui + + + + 0 + 0 + 324 + 132 + + + + + 0 + 0 + + + + + 320 + 132 + + + + + Sans Serif + 9 + + + + AirspyHF + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 4 + + + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Toggle record I/Q samples from device + + + + + + + :/record_off.png:/record_off.png + + + + + + + + + + + I/Q sample rate kS/s + + + 00000k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + DejaVu Sans Mono + 20 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Tuner center frequency in kHz + + + + + + + kHz + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + + LO ppm + + + + + + + Local Oscillator ppm correction + + + -100 + + + 100 + + + 1 + + + Qt::Horizontal + + + + + + + + 36 + 0 + + + + LO correction value (ppm) + + + -00.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 24 + 16777215 + + + + Rest LO ppm correction + + + R + + + + + + + + + Qt::Horizontal + + + + + + + + + Band + + + + + + + + 56 + 0 + + + + Band select + + + + HF + + + + + VHF + + + + + + + + + 0 + 0 + + + + S/R + + + + + + + + 60 + 0 + + + + + 60 + 16777215 + + + + Device sample rate in MS/s + + + + 0000 + + + + + + + + k + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Dec + + + + + + + + 50 + 16777215 + + + + Decimation factor + + + 0 + + + + 1 + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + 32 + + + + + + + + + 24 + 24 + + + + Transverter frequency translation dialog + + + X + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + ValueDial + QWidget +
gui/valuedial.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + TransverterButton + QPushButton +
gui/transverterbutton.h
+
+
+ + + + +
diff --git a/plugins/samplesource/airspyhff/airspyhffinput.cpp b/plugins/samplesource/airspyhff/airspyhffinput.cpp new file mode 100644 index 000000000..0fde45a7b --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffinput.cpp @@ -0,0 +1,492 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// 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 "SWGDeviceSettings.h" +#include "SWGDeviceState.h" + +#include +#include +#include "dsp/dspcommands.h" +#include "dsp/dspengine.h" + +#include "airspyhffinput.h" + +#include "airspyhffgui.h" +#include "airspyhffplugin.h" +#include "airspyhffsettings.h" +#include "airspyhffthread.h" + +MESSAGE_CLASS_DEFINITION(AirspyHFFInput::MsgConfigureAirspyHF, Message) +MESSAGE_CLASS_DEFINITION(AirspyHFFInput::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(AirspyHFFInput::MsgFileRecord, Message) + +const qint64 AirspyHFFInput::loLowLimitFreqHF = 9000L; +const qint64 AirspyHFFInput::loHighLimitFreqHF = 31000000L; +const qint64 AirspyHFFInput::loLowLimitFreqVHF = 60000000L; +const qint64 AirspyHFFInput::loHighLimitFreqVHF = 260000000L; + +AirspyHFFInput::AirspyHFFInput(DeviceSourceAPI *deviceAPI) : + m_deviceAPI(deviceAPI), + m_settings(), + m_dev(0), + m_airspyHFThread(0), + m_deviceDescription("AirspyHFF"), + m_running(false) +{ + openDevice(); + + char recFileNameCStr[30]; + sprintf(recFileNameCStr, "test_%d.sdriq", m_deviceAPI->getDeviceUID()); + m_fileSink = new FileRecord(std::string(recFileNameCStr)); + m_deviceAPI->addSink(m_fileSink); +} + +AirspyHFFInput::~AirspyHFFInput() +{ + if (m_running) { stop(); } + m_deviceAPI->removeSink(m_fileSink); + delete m_fileSink; + closeDevice(); +} + +void AirspyHFFInput::destroy() +{ + delete this; +} + +bool AirspyHFFInput::openDevice() +{ + if (m_dev != 0) + { + closeDevice(); + } + + airspyhf_error rc; + + if (!m_sampleFifo.setSize(1<<19)) + { + qCritical("AirspyHFFInput::start: could not allocate SampleFifo"); + return false; + } + + if ((m_dev = open_airspyhf_from_serial(m_deviceAPI->getSampleSourceSerial())) == 0) + { + qCritical("AirspyHFFInput::start: could not open Airspy HF with serial %s", qPrintable(m_deviceAPI->getSampleSourceSerial())); + return false; + } + else + { + qDebug("AirspyHFFInput::start: opened Airspy HF with serial %s", qPrintable(m_deviceAPI->getSampleSourceSerial())); + } + + uint32_t nbSampleRates; + uint32_t *sampleRates; + + rc = (airspyhf_error) airspyhf_get_samplerates(m_dev, &nbSampleRates, 0); + + if (rc == AIRSPYHF_SUCCESS) + { + qDebug("AirspyHFFInput::start: %d sample rates for Airspy HF", nbSampleRates); + } + else + { + qCritical("AirspyHFFInput::start: could not obtain the number of Airspy HF sample rates"); + return false; + } + + sampleRates = new uint32_t[nbSampleRates]; + + rc = (airspyhf_error) airspyhf_get_samplerates(m_dev, sampleRates, nbSampleRates); + + if (rc == AIRSPYHF_SUCCESS) + { + qDebug("AirspyHFFInput::start: obtained Airspy HF sample rates"); + } + else + { + qCritical("AirspyHFFInput::start: could not obtain Airspy HF sample rates"); + return false; + } + + m_sampleRates.clear(); + + for (unsigned int i = 0; i < nbSampleRates; i++) + { + m_sampleRates.push_back(sampleRates[i]); + qDebug("AirspyHFFInput::start: sampleRates[%d] = %u Hz", i, sampleRates[i]); + } + + delete[] sampleRates; + + airspyhf_set_sample_type(m_dev, AIRSPYHF_SAMPLE_INT16_IQ); + + return true; +} + +void AirspyHFFInput::init() +{ + applySettings(m_settings, true); +} + +bool AirspyHFFInput::start() +{ + QMutexLocker mutexLocker(&m_mutex); + + if (!m_dev) { + return false; + } + + if (m_running) { stop(); } + + if ((m_airspyHFThread = new AirspyHFFThread(m_dev, &m_sampleFifo)) == 0) + { + qFatal("AirspyHFInput::start: out of memory"); + stop(); + return false; + } + + m_airspyHFThread->setSamplerate(m_sampleRates[m_settings.m_devSampleRateIndex]); + m_airspyHFThread->setLog2Decimation(m_settings.m_log2Decim); + + m_airspyHFThread->startWork(); + + mutexLocker.unlock(); + + applySettings(m_settings, true); + + qDebug("AirspyHFInput::startInput: started"); + m_running = true; + + return true; +} + +void AirspyHFFInput::closeDevice() +{ + if (m_dev != 0) + { + airspyhf_stop(m_dev); + airspyhf_close(m_dev); + m_dev = 0; + } + + m_deviceDescription.clear(); +} + +void AirspyHFFInput::stop() +{ + qDebug("AirspyHFInput::stop"); + QMutexLocker mutexLocker(&m_mutex); + + if (m_airspyHFThread != 0) + { + m_airspyHFThread->stopWork(); + delete m_airspyHFThread; + m_airspyHFThread = 0; + } + + m_running = false; +} + +QByteArray AirspyHFFInput::serialize() const +{ + return m_settings.serialize(); +} + +bool AirspyHFFInput::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureAirspyHF* message = MsgConfigureAirspyHF::create(m_settings, true); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) + { + MsgConfigureAirspyHF* messageToGUI = MsgConfigureAirspyHF::create(m_settings, true); + m_guiMessageQueue->push(messageToGUI); + } + + return success; +} + +const QString& AirspyHFFInput::getDeviceDescription() const +{ + return m_deviceDescription; +} + +int AirspyHFFInput::getSampleRate() const +{ + int rate = m_sampleRates[m_settings.m_devSampleRateIndex]; + return (rate / (1<push(messageToGUI); + } +} + +bool AirspyHFFInput::handleMessage(const Message& message) +{ + if (MsgConfigureAirspyHF::match(message)) + { + MsgConfigureAirspyHF& conf = (MsgConfigureAirspyHF&) message; + qDebug() << "MsgConfigureAirspyHF::handleMessage: MsgConfigureAirspyHF"; + + bool success = applySettings(conf.getSettings(), conf.getForce()); + + if (!success) + { + qDebug("MsgConfigureAirspyHF::handleMessage: AirspyHF config error"); + } + + return true; + } + else if (MsgStartStop::match(message)) + { + MsgStartStop& cmd = (MsgStartStop&) message; + qDebug() << "AirspyHFInput::handleMessage: MsgStartStop: " << (cmd.getStartStop() ? "start" : "stop"); + + if (cmd.getStartStop()) + { + if (m_deviceAPI->initAcquisition()) + { + m_deviceAPI->startAcquisition(); + DSPEngine::instance()->startAudioOutput(); + } + } + else + { + m_deviceAPI->stopAcquisition(); + DSPEngine::instance()->stopAudioOutput(); + } + + return true; + } + else if (MsgFileRecord::match(message)) + { + MsgFileRecord& conf = (MsgFileRecord&) message; + qDebug() << "AirspyHFInput::handleMessage: MsgFileRecord: " << conf.getStartStop(); + + if (conf.getStartStop()) { + m_fileSink->startRecording(); + } else { + m_fileSink->stopRecording(); + } + + return true; + } + else + { + return false; + } +} + +void AirspyHFFInput::setDeviceCenterFrequency(quint64 freq_hz, const AirspyHFFSettings& settings) +{ + switch(settings.m_bandIndex) + { + case 1: + freq_hz = freq_hz < loLowLimitFreqVHF ? loLowLimitFreqVHF : freq_hz > loHighLimitFreqVHF ? loHighLimitFreqVHF : freq_hz; + break; + case 0: + default: + freq_hz = freq_hz < loLowLimitFreqHF ? loLowLimitFreqHF : freq_hz > loHighLimitFreqHF ? loHighLimitFreqHF : freq_hz; + break; + } + + airspyhf_error rc = (airspyhf_error) airspyhf_set_freq(m_dev, static_cast(freq_hz)); + + if (rc == AIRSPYHF_SUCCESS) { + qDebug("AirspyHFInput::setDeviceCenterFrequency: frequency set to %llu Hz", freq_hz); + } else { + qWarning("AirspyHFInput::setDeviceCenterFrequency: could not frequency to %llu Hz", freq_hz); + } +} + +bool AirspyHFFInput::applySettings(const AirspyHFFSettings& settings, bool force) +{ + QMutexLocker mutexLocker(&m_mutex); + + bool forwardChange = false; + airspyhf_error rc; + int sampleRateIndex = settings.m_devSampleRateIndex; + + qDebug() << "AirspyHFInput::applySettings"; + + if ((m_settings.m_devSampleRateIndex != settings.m_devSampleRateIndex) || force) + { + forwardChange = true; + + if (settings.m_devSampleRateIndex >= m_sampleRates.size()) { + sampleRateIndex = m_sampleRates.size() - 1; + } + + if (m_dev != 0) + { + rc = (airspyhf_error) airspyhf_set_samplerate(m_dev, sampleRateIndex); + + if (rc != AIRSPYHF_SUCCESS) + { + qCritical("AirspyHFInput::applySettings: could not set sample rate index %u (%d S/s)", sampleRateIndex, m_sampleRates[sampleRateIndex]); + } + else if (m_airspyHFThread != 0) + { + qDebug("AirspyHFInput::applySettings: sample rate set to index: %u (%d S/s)", sampleRateIndex, m_sampleRates[sampleRateIndex]); + m_airspyHFThread->setSamplerate(m_sampleRates[sampleRateIndex]); + } + } + } + + if ((m_settings.m_log2Decim != settings.m_log2Decim) || force) + { + forwardChange = true; + + if (m_airspyHFThread != 0) + { + m_airspyHFThread->setLog2Decimation(settings.m_log2Decim); + qDebug() << "AirspyInput: set decimation to " << (1<handleMessage(*notif); // forward to file sink + m_deviceAPI->getDeviceEngineInputMessageQueue()->push(notif); + } + + m_settings = settings; + m_settings.m_devSampleRateIndex = sampleRateIndex; + return true; +} + +airspyhf_device_t *AirspyHFFInput::open_airspyhf_from_serial(const QString& serialStr) +{ + airspyhf_device_t *devinfo; + bool ok; + airspyhf_error rc; + + uint64_t serial = serialStr.toULongLong(&ok, 16); + + if (!ok) + { + qCritical("AirspyHFInput::open_airspyhf_from_serial: invalid serial %s", qPrintable(serialStr)); + return 0; + } + else + { + rc = (airspyhf_error) airspyhf_open_sn(&devinfo, serial); + + if (rc == AIRSPYHF_SUCCESS) { + return devinfo; + } else { + return 0; + } + } +} + +int AirspyHFFInput::webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage __attribute__((unused))) +{ + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + return 200; +} + +int AirspyHFFInput::webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage __attribute__((unused))) +{ + 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; +} + diff --git a/plugins/samplesource/airspyhff/airspyhffinput.h b/plugins/samplesource/airspyhff/airspyhffinput.h new file mode 100644 index 000000000..286a81648 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffinput.h @@ -0,0 +1,147 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AIRSPYHFFINPUT_H +#define INCLUDE_AIRSPYHFFINPUT_H + +#include +#include + +#include +#include + +#include "airspyhffsettings.h" + +class DeviceSourceAPI; +class AirspyHFFThread; +class FileRecord; + +class AirspyHFFInput : public DeviceSampleSource { +public: + class MsgConfigureAirspyHF : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AirspyHFFSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAirspyHF* create(const AirspyHFFSettings& settings, bool force) + { + return new MsgConfigureAirspyHF(settings, force); + } + + private: + AirspyHFFSettings m_settings; + bool m_force; + + MsgConfigureAirspyHF(const AirspyHFFSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgFileRecord : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgFileRecord* create(bool startStop) { + return new MsgFileRecord(startStop); + } + + protected: + bool m_startStop; + + MsgFileRecord(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + 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) + { } + }; + + AirspyHFFInput(DeviceSourceAPI *deviceAPI); + virtual ~AirspyHFFInput(); + 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 quint64 getCenterFrequency() const; + virtual void setCenterFrequency(qint64 centerFrequency); + const std::vector& getSampleRates() const { return m_sampleRates; } + + virtual bool handleMessage(const Message& message); + + virtual int webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + static const qint64 loLowLimitFreqHF; + static const qint64 loHighLimitFreqHF; + static const qint64 loLowLimitFreqVHF; + static const qint64 loHighLimitFreqVHF; + +private: + bool openDevice(); + void closeDevice(); + bool applySettings(const AirspyHFFSettings& settings, bool force); + airspyhf_device_t *open_airspyhf_from_serial(const QString& serialStr); + void setDeviceCenterFrequency(quint64 freq, const AirspyHFFSettings& settings); + + DeviceSourceAPI *m_deviceAPI; + QMutex m_mutex; + AirspyHFFSettings m_settings; + airspyhf_device_t* m_dev; + AirspyHFFThread* m_airspyHFThread; + QString m_deviceDescription; + std::vector m_sampleRates; + bool m_running; + FileRecord *m_fileSink; //!< File sink to record device I/Q output +}; + +#endif // INCLUDE_AIRSPYHFFINPUT_H diff --git a/plugins/samplesource/airspyhff/airspyhffplugin.cpp b/plugins/samplesource/airspyhff/airspyhffplugin.cpp new file mode 100644 index 000000000..146e27063 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffplugin.cpp @@ -0,0 +1,126 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// 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 "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "airspyhffplugin.h" +#include "airspyhffgui.h" + + +const PluginDescriptor AirspyHFFPlugin::m_pluginDescriptor = { + QString("AirspyHF Input (float)"), + QString("3.12.0"), + QString("(c) Edouard Griffiths, F4EXB"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +const QString AirspyHFFPlugin::m_hardwareID = "AirspyHFF"; +const QString AirspyHFFPlugin::m_deviceTypeID = AIRSPYHFF_DEVICE_TYPE_ID; +const int AirspyHFFPlugin::m_maxDevices = 32; + +AirspyHFFPlugin::AirspyHFFPlugin(QObject* parent) : + QObject(parent) +{ +} + +const PluginDescriptor& AirspyHFFPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void AirspyHFFPlugin::initPlugin(PluginAPI* pluginAPI) +{ + pluginAPI->registerSampleSource(m_deviceTypeID, this); +} + +PluginInterface::SamplingDevices AirspyHFFPlugin::enumSampleSources() +{ + SamplingDevices result; + int nbDevices; + uint64_t deviceSerials[m_maxDevices]; + + nbDevices = airspyhf_list_devices(deviceSerials, m_maxDevices); + + if (nbDevices < 0) + { + qCritical("AirspyHFPlugin::enumSampleSources: failed to list Airspy HF devices"); + } + + for (int i = 0; i < nbDevices; i++) + { + if (deviceSerials[i]) + { + QString serial_str = QString::number(deviceSerials[i], 16); + QString displayedName(QString("AirspyHF(float)[%1] %2").arg(i).arg(serial_str)); + + result.append(SamplingDevice(displayedName, + m_hardwareID, + m_deviceTypeID, + serial_str, + i, + PluginInterface::SamplingDevice::PhysicalDevice, + true, + 1, + 0)); + + qDebug("AirspyHFFPlugin::enumSampleSources: enumerated Airspy HF device #%d", i); + } + else + { + qDebug("AirspyHFFPlugin::enumSampleSources: finished to enumerate Airspy HF. %d devices found", i); + break; // finished + } + } + + return result; +} + +PluginInstanceGUI* AirspyHFFPlugin::createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + if (sourceId == m_deviceTypeID) + { + AirspyHFFGui* gui = new AirspyHFFGui(deviceUISet); + *widget = gui; + return gui; + } + else + { + return 0; + } +} + +DeviceSampleSource *AirspyHFFPlugin::createSampleSourcePluginInstanceInput(const QString& sourceId, DeviceSourceAPI *deviceAPI) +{ + if (sourceId == m_deviceTypeID) + { + AirspyHFFInput* input = new AirspyHFFInput(deviceAPI); + return input; + } + else + { + return 0; + } +} diff --git a/plugins/samplesource/airspyhff/airspyhffplugin.h b/plugins/samplesource/airspyhff/airspyhffplugin.h new file mode 100644 index 000000000..423ae6519 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffplugin.h @@ -0,0 +1,53 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AIRSPYHFFPLUGIN_H +#define INCLUDE_AIRSPYHFFPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +#define AIRSPYHFF_DEVICE_TYPE_ID "sdrangel.samplesource.airspyhff" + +class PluginAPI; + +class AirspyHFFPlugin : public QObject, public PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID AIRSPYHFF_DEVICE_TYPE_ID) + +public: + explicit AirspyHFFPlugin(QObject* parent = 0); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual SamplingDevices enumSampleSources(); + virtual PluginInstanceGUI* createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet); + virtual DeviceSampleSource* createSampleSourcePluginInstanceInput(const QString& sourceId, DeviceSourceAPI *deviceAPI); + + static const QString m_hardwareID; + static const QString m_deviceTypeID; + static const int m_maxDevices; + +private: + static const PluginDescriptor m_pluginDescriptor; +}; + +#endif // INCLUDE_AIRSPYHFFPLUGIN_H diff --git a/plugins/samplesource/airspyhff/airspyhffsettings.cpp b/plugins/samplesource/airspyhff/airspyhffsettings.cpp new file mode 100644 index 000000000..f09ee9c42 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffsettings.cpp @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// 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 "airspyhffsettings.h" + +AirspyHFFSettings::AirspyHFFSettings() +{ + resetToDefaults(); +} + +void AirspyHFFSettings::resetToDefaults() +{ + m_centerFrequency = 7150*1000; + m_LOppmTenths = 0; + m_devSampleRateIndex = 0; + m_log2Decim = 0; + m_transverterMode = false; + m_transverterDeltaFrequency = 0; + m_bandIndex = 0; +} + +QByteArray AirspyHFFSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeU32(1, m_devSampleRateIndex); + s.writeS32(2, m_LOppmTenths); + s.writeU32(3, m_log2Decim); + s.writeBool(7, m_transverterMode); + s.writeS64(8, m_transverterDeltaFrequency); + s.writeU32(9, m_bandIndex); + + return s.final(); +} + +bool AirspyHFFSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + int intval; + quint32 uintval; + + d.readU32(1, &m_devSampleRateIndex, 0); + d.readS32(2, &m_LOppmTenths, 0); + d.readU32(3, &m_log2Decim, 0); + d.readS32(4, &intval, 0); + d.readBool(7, &m_transverterMode, false); + d.readS64(8, &m_transverterDeltaFrequency, 0); + d.readU32(9, &uintval, 0); + m_bandIndex = uintval > 1 ? 1 : uintval; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/samplesource/airspyhff/airspyhffsettings.h b/plugins/samplesource/airspyhff/airspyhffsettings.h new file mode 100644 index 000000000..1a9d2a691 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffsettings.h @@ -0,0 +1,36 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// 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 _AIRSPYHFF_AIRSPYHFFSETTINGS_H_ +#define _AIRSPYHFF_AIRSPYHFFSETTINGS_H_ + +struct AirspyHFFSettings +{ + quint64 m_centerFrequency; + qint32 m_LOppmTenths; + quint32 m_devSampleRateIndex; + quint32 m_log2Decim; + bool m_transverterMode; + qint64 m_transverterDeltaFrequency; + quint32 m_bandIndex; + + AirspyHFFSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* _AIRSPYHFF_AIRSPYHFFSETTINGS_H_ */ diff --git a/plugins/samplesource/airspyhff/airspyhffthread.cpp b/plugins/samplesource/airspyhff/airspyhffthread.cpp new file mode 100644 index 000000000..ebafee037 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffthread.cpp @@ -0,0 +1,142 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// 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 "dsp/samplesinkfifo.h" +#include "airspyhffthread.h" + +AirspyHFFThread *AirspyHFFThread::m_this = 0; + +AirspyHFFThread::AirspyHFFThread(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, QObject* parent) : + QThread(parent), + m_running(false), + m_dev(dev), + m_convertBuffer(AIRSPYHFF_BLOCKSIZE), + m_sampleFifo(sampleFifo), + m_samplerate(10), + m_log2Decim(0) +{ + m_this = this; +} + +AirspyHFFThread::~AirspyHFFThread() +{ + stopWork(); + m_this = 0; +} + +void AirspyHFFThread::startWork() +{ + m_startWaitMutex.lock(); + start(); + while(!m_running) + m_startWaiter.wait(&m_startWaitMutex, 100); + m_startWaitMutex.unlock(); +} + +void AirspyHFFThread::stopWork() +{ + qDebug("AirspyThread::stopWork"); + m_running = false; + wait(); +} + +void AirspyHFFThread::setSamplerate(uint32_t samplerate) +{ + m_samplerate = samplerate; +} + +void AirspyHFFThread::setLog2Decimation(unsigned int log2_decim) +{ + m_log2Decim = log2_decim; +} + +void AirspyHFFThread::run() +{ + airspyhf_error rc; + + m_running = true; + m_startWaiter.wakeAll(); + + rc = (airspyhf_error) airspyhf_start(m_dev, rx_callback, 0); + + if (rc != AIRSPYHF_SUCCESS) + { + qCritical("AirspyHFFThread::run: failed to start Airspy HF Rx"); + } + else + { + while ((m_running) && (airspyhf_is_streaming(m_dev) != 0)) + { + sleep(1); + } + } + + rc = (airspyhf_error) airspyhf_stop(m_dev); + + if (rc == AIRSPYHF_SUCCESS) { + qDebug("AirspyHFFThread::run: stopped Airspy HF Rx"); + } else { + qDebug("AirspyHFFThread::run: failed to stop Airspy HF Rx"); + } + + m_running = false; +} + +// Decimate according to specified log2 (ex: log2=4 => decim=16) +void AirspyHFFThread::callback(const qint16* buf, qint32 len) +{ + SampleVector::iterator it = m_convertBuffer.begin(); + + switch (m_log2Decim) + { + case 0: + m_decimators.decimate1(&it, buf, len); + break; + case 1: + m_decimators.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimators.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimators.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimators.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimators.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimators.decimate64_cen(&it, buf, len); + break; + default: + break; + } + + m_sampleFifo->write(m_convertBuffer.begin(), it); +} + + +int AirspyHFFThread::rx_callback(airspyhf_transfer_t* transfer) +{ + qint32 bytes_to_write = transfer->sample_count * sizeof(qint16); + m_this->callback((qint16 *) transfer->samples, bytes_to_write); + return 0; +} diff --git a/plugins/samplesource/airspyhff/airspyhffthread.h b/plugins/samplesource/airspyhff/airspyhffthread.h new file mode 100644 index 000000000..998dc6470 --- /dev/null +++ b/plugins/samplesource/airspyhff/airspyhffthread.h @@ -0,0 +1,67 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018 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 // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AIRSPYHFFTHREAD_H +#define INCLUDE_AIRSPYHFFTHREAD_H + +#include +#include +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/decimators.h" + +#define AIRSPYHFF_BLOCKSIZE (1<<17) + +class AirspyHFFThread : public QThread { + Q_OBJECT + +public: + AirspyHFFThread(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, QObject* parent = 0); + ~AirspyHFFThread(); + + void startWork(); + void stopWork(); + void setSamplerate(uint32_t samplerate); + void setLog2Decimation(unsigned int log2_decim); + +private: + QMutex m_startWaitMutex; + QWaitCondition m_startWaiter; + bool m_running; + + airspyhf_device_t* m_dev; + qint16 m_buf[2*AIRSPYHFF_BLOCKSIZE]; + SampleVector m_convertBuffer; + SampleSinkFifo* m_sampleFifo; + + int m_samplerate; + unsigned int m_log2Decim; + static AirspyHFFThread *m_this; + +#ifdef SDR_RX_SAMPLE_24BIT + Decimators m_decimators; +#else + Decimators m_decimators; +#endif + + void run(); + void callback(const qint16* buf, qint32 len); + static int rx_callback(airspyhf_transfer_t* transfer); +}; + +#endif // INCLUDE_AIRSPYHFFTHREAD_H diff --git a/plugins/samplesource/airspyhff/readme.md b/plugins/samplesource/airspyhff/readme.md new file mode 100644 index 000000000..6af205a0d --- /dev/null +++ b/plugins/samplesource/airspyhff/readme.md @@ -0,0 +1,105 @@ +

AirspyHF input plugin

+ +

Introduction

+ +This input sample source plugin gets its samples from a [Airspy HF+ device](https://airspy.com/airspy-hf-plus/). + +

Build

+ +The plugin will be built only if the [Airspy HF library](https://github.com/f4exb/airspyhf) is installed in your system. Please note that you should use my fork as it deals with integer samples. The branch to check out is `intsamples` but this is the default in Github. + +If you build it from source and install it in a custom location say: `/opt/install/libairspyhf` you will have to add `-DLIBRTLSDR_INCLUDE_DIR=/opt/install/libairspyhf/include -DLIBRTLSDR_LIBRARIES=/opt/install/libairspyhf/lib/libairspyhf.so` to the cmake command line. + +Note: if you use binary distributions this is included in the bundle. + +

Interface

+ +It has very few controls compared to other source interfaces. This is because a lot of things are handled automatically with the AirspyHF+: + + - gains (hardware) + - DC and IQ correction (library software) + +![AirspyHF input plugin GUI](../../../doc/img/AirspyHFInput_plugin.png) + +

1: Common stream parameters

+ +![SDR Daemon source input stream GUI](../../../doc/img/SDRdaemonSource_plugin_01.png) + +

1.1: Frequency

+ +This is the center frequency of reception in kHz. + +

1.2: Start/Stop

+ +Device start / stop button. + + - Blue triangle icon: device is ready and can be started + - Green square icon: device is running and can be stopped + - Magenta (or pink) square icon: an error occured. In the case the device was accidentally disconnected you may click on the icon, plug back in and start again. + +

1.3: Record

+ +Record baseband I/Q stream toggle button + +

1.4: Stream sample rate

+ +Baseband I/Q sample rate in kS/s. This is the device to host sample rate (3) divided by the decimation factor (4). + +

2: Lo ppm correction

+ +This is the correction factor in ppm applied to the local oscillator. The Airspy HF LO has 1 kHz increments so anything in between is obtained by mixing the signal with a Hz precision NCO. This is actually done in the AirspyHF library. + +On HF band the LO correction is not necessary because the LO is largely precise enough for the frequencies involved. You can disable the NCO in AirspyHF library by setting the value to zero. Since the LO control in SDRangel has a 1 kHz step the NCO correction will always be zero. In AirspyHF library (my fork) the NCO is not active (no extra complex multiplication) if the correction is zero. On HF band it is recommended not to use the LO correction (set it or leave it at 0). + +You can reset the ppm value anytime by pressing on button (3) + +

3: Reset LO ppm correction

+ +THis resets the LO ppm correction (zero the value). By doing so the LO trimming NCO in AirspyHF libray is disabled. + +

4: Band select

+ +Use this combo box to select the HF or VHF range. This will set the limits of the frequency dial (1.1) appropriately and possibly move the current frequency inside the limits. Limits are given by the AirspyHF+ specifications: + + - HF: 9 kHz to 31 MHz + - VHF: 60 to 260 MHz + +

5: Device to hast sample rate

+ +This is the device to host sample rate in samples per second (S/s). + +Although the combo box is there to present a choice of sample rates at present the AirspyHF+ deals only with 768 kS/s. However the support library has provision to get a list of sample rates from the device incase of future developments. + +

6: Decimation factor

+ +The I/Q stream from the AirspyHF to host is downsampled by a power of two before being sent to the passband. Possible values are increasing powers of two: 1 (no decimation), 2, 4, 8, 16, 32, 64. When using audio channel plugins (AM, DSD, NFM, SSB...) please make sure that the sample rate is not less than 48 kHz (no decimation by 32 or 64). + +

7: Transverter mode open dialog

+ +This button opens a dialog to set the transverter mode frequency translation options: + +![SDR Daemon source input stream trasverter dialog](../../../doc/img/RTLSDR_plugin_xvrt.png) + +Note that if you mouse over the button a tooltip appears that displays the translating frequency and if translation is enabled or disabled. When the frequency translation is enabled the button is lit. + +

7a.1: Translating frequency

+ +You can set the translating frequency in Hz with this dial. Use the wheels to adjust the sample rate. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arroews. Pressing shift simultanoeusly moves digit by 5 and pressing control moves it by 2. + +The frequency set in the device is the frequency on the main dial (1) minus this frequency. Thus it is positive for down converters and negative for up converters. + +For example with the DX Patrol that has a mixer at 120 MHz for HF operation you would set the value to -120,000,000 Hz so that if the main dial frequency is set at 7,130 kHz the RTLSDR of the DX Patrol will be set to 127.130 MHz. + +If you use a down converter to receive the 6 cm band narrowband center frequency of 5670 MHz at 432 MHz you would set the translating frequency to 5760 - 432 = 5328 MHz thus dial +5,328,000,000 Hz. + +For bands even higher in the frequency spectrum the GHz digits are not really significant so you can have them set at 1 GHz. Thus to receive the 10368 MHz frequency at 432 MHz you would set the translating frequency to 1368 - 432 = 936 MHz. Note that in this case the frequency of the LO used in the mixer of the transverter is set at 9936 MHz. + +The Hz precision allows a fine tuning of the transverter LO offset + +

7a.2: Translating frequency enable/disable

+ +Use this toggle button to activate or deactivate the frequency translation + +

7a.3: Confirmation buttons

+ +Use these buttons to confirm ("OK") or dismiss ("Cancel") your changes.