diff --git a/CMakeLists.txt b/CMakeLists.txt
index ccb1e60a7..b153e0148 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -271,6 +271,7 @@ if (BUILD_DEBIAN)
add_subdirectory(serialdv)
add_subdirectory(dsdcc)
add_subdirectory(libairspy)
+ add_subdirectory(libairspyhf)
add_subdirectory(libhackrf)
add_subdirectory(librtlsdr)
add_subdirectory(libbladerf)
diff --git a/cmake/Modules/FindLibAIRSPYHF.cmake b/cmake/Modules/FindLibAIRSPYHF.cmake
new file mode 100644
index 000000000..58ca04388
--- /dev/null
+++ b/cmake/Modules/FindLibAIRSPYHF.cmake
@@ -0,0 +1,28 @@
+if(NOT LIBAIRSPYHF_FOUND)
+
+ pkg_check_modules (LIBAIRSPYHF_PKG libairspyhf)
+ find_path(LIBAIRSPYHF_INCLUDE_DIR NAMES libairspyhf/airspyhf.h
+ PATHS
+ ${LIBAIRSPYHF_PKG_INCLUDE_DIRS}
+ /usr/include
+ /usr/local/include
+ )
+
+ find_library(LIBAIRSPYHF_LIBRARIES NAMES airspyhf
+ PATHS
+ ${LIBAIRSPYHF_PKG_LIBRARY_DIRS}
+ /usr/lib
+ /usr/local/lib
+ )
+
+ if(LIBAIRSPYHF_INCLUDE_DIR AND LIBAIRSPYHF_LIBRARIES)
+ set(LIBAIRSPYHF_FOUND TRUE CACHE INTERNAL "libairspyhf found")
+ message(STATUS "Found libairspyhf: ${LIBAIRSPYHF_INCLUDE_DIR}, ${LIBAIRSPYHF_LIBRARIES}")
+ else(LIBAIRSPYHF_INCLUDE_DIR AND LIBAIRSPYHF_LIBRARIES)
+ set(LIBAIRSPYHF_FOUND FALSE CACHE INTERNAL "libairspyhf found")
+ message(STATUS "libairspyhf not found.")
+ endif(LIBAIRSPYHF_INCLUDE_DIR AND LIBAIRSPYHF_LIBRARIES)
+
+ mark_as_advanced(LIBAIRSPYHF_INCLUDE_DIR LIBAIRSPYHF_LIBRARIES)
+
+endif(NOT LIBAIRSPYHF_FOUND)
diff --git a/libairspyhf/CMakeLists.txt b/libairspyhf/CMakeLists.txt
new file mode 100644
index 000000000..645d76fae
--- /dev/null
+++ b/libairspyhf/CMakeLists.txt
@@ -0,0 +1,33 @@
+project(airspyhf)
+
+find_package(LibUSB)
+
+set(airspyhf_SOURCES
+ ${LIBAIRSPYHFSRC}/libairspyhf/src/airspyhf.c
+ ${LIBAIRSPYHFSRC}/libairspyhf/src/iqbalancer.c
+)
+
+set(airspyhf_HEADERS
+ ${LIBAIRSPYHFSRC}/libairspyhf/src/airspyhf.h
+ ${LIBAIRSPYHFSRC}/libairspyhf/src/airspyhf_commands.h
+ ${LIBAIRSPYHFSRC}/libairspyhf/src/iqbalancer.h
+)
+
+include_directories(
+ .
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${LIBUSB_INCLUDE_DIR}
+ ${LIBAIRSPYHFSRC}/libairspyhf/src
+)
+
+add_definitions(-DQT_SHARED)
+
+add_library(airspyhf SHARED
+ ${airspyhf_SOURCES}
+)
+
+target_link_libraries(airspyhf
+ ${LIBUSB_LIBRARIES}
+)
+
+install(TARGETS airspyhf DESTINATION lib)
diff --git a/libairspyhf/libairspyhf.pro b/libairspyhf/libairspyhf.pro
new file mode 100644
index 000000000..9e1ec479a
--- /dev/null
+++ b/libairspyhf/libairspyhf.pro
@@ -0,0 +1,30 @@
+#--------------------------------------------------------
+#
+# Pro file for Android and Windows builds with Qt Creator
+#
+#--------------------------------------------------------
+
+QT += core
+
+TEMPLATE = lib
+TARGET = libairspyhf
+
+CONFIG(MINGW32):LIBAIRSPYHFSRC = "D:\softs\libairspyhf\libairspyhf"
+CONFIG(MINGW64):LIBAIRSPYHFSRC = "D:\softs\libairspyhf\libairspyhf"
+INCLUDEPATH += $$LIBAIRSPYHFSRC/src
+
+CONFIG(MINGW32):INCLUDEPATH += "D:\softs\libusb-1.0.20\include\libusb-1.0"
+CONFIG(MINGW64):INCLUDEPATH += "D:\softs\libusb-1.0.20\include\libusb-1.0"
+
+SOURCES = $$LIBAIRSPYSRC/src/airspyhf.c\
+ $$LIBAIRSPYSRC/src/iqbalancer.c
+
+HEADERS = $$LIBAIRSPYSRC/src/airspyhf.h\
+ $$LIBAIRSPYSRC/src/airspyhf_commands.h\
+ $$LIBAIRSPYSRC/src/iqbalancer.h
+
+CONFIG(MINGW32):LIBS += -LD:\softs\libusb-1.0.20\MinGW32\dll -llibusb-1.0
+CONFIG(MINGW64):LIBS += -LD:\softs\libusb-1.0.20\MinGW64\dll -llibusb-1.0
+
+CONFIG(ANDROID):CONFIG += mobility
+CONFIG(ANDROID):MOBILITY =
diff --git a/plugins/samplesource/CMakeLists.txt b/plugins/samplesource/CMakeLists.txt
index 7e55eb0fb..98f52f9a3 100644
--- a/plugins/samplesource/CMakeLists.txt
+++ b/plugins/samplesource/CMakeLists.txt
@@ -37,6 +37,11 @@ if(LIBUSB_FOUND AND LIBAIRSPY_FOUND)
add_subdirectory(airspy)
endif(LIBUSB_FOUND AND LIBAIRSPY_FOUND)
+find_package(LibAIRSPYHF)
+if(LIBUSB_FOUND AND LIBAIRSPYHF_FOUND)
+ add_subdirectory(airspyhf)
+endif(LIBUSB_FOUND AND LIBAIRSPYHF_FOUND)
+
find_package(LibHACKRF)
if(LIBUSB_FOUND AND LIBHACKRF_FOUND)
add_subdirectory(hackrfinput)
@@ -71,6 +76,7 @@ if (BUILD_DEBIAN)
add_subdirectory(sdrdaemonsource)
endif (LIBNANOMSG_FOUND)
add_subdirectory(airspy)
+ add_subdirectory(airspyhf)
add_subdirectory(hackrfinput)
add_subdirectory(rtlsdr)
add_subdirectory(bladerfinput)
diff --git a/plugins/samplesource/airspyhf/CMakeLists.txt b/plugins/samplesource/airspyhf/CMakeLists.txt
new file mode 100644
index 000000000..915d145ce
--- /dev/null
+++ b/plugins/samplesource/airspyhf/CMakeLists.txt
@@ -0,0 +1,78 @@
+project(airspyhf)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+
+set(airspyhf_SOURCES
+ airspyhfgui.cpp
+ airspyhfinput.cpp
+ airspyhfplugin.cpp
+ airspyhfsettings.cpp
+ airspyhfthread.cpp
+)
+
+set(airspyhf_HEADERS
+ airspyhfgui.h
+ airspyhfinput.h
+ airspyhfplugin.h
+ airspyhfsettings.h
+ airspyhfthread.h
+)
+
+set(airspyhf_FORMS
+ airspyhfgui.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(airspyhf_HEADERS_MOC ${airspyhf_HEADERS})
+qt5_wrap_ui(airspyhf_FORMS_HEADERS ${airspyhf_FORMS})
+
+add_library(inputairspyhf SHARED
+ ${airspyhf_SOURCES}
+ ${airspyhf_HEADERS_MOC}
+ ${airspyhf_FORMS_HEADERS}
+)
+
+if (BUILD_DEBIAN)
+target_link_libraries(inputairspyhf
+ ${QT_LIBRARIES}
+ airspyhf
+ sdrbase
+ sdrgui
+ swagger
+)
+else (BUILD_DEBIAN)
+target_link_libraries(inputairspyhf
+ ${QT_LIBRARIES}
+ ${LIBAIRSPYHF_LIBRARIES}
+ sdrbase
+ sdrgui
+ swagger
+)
+endif (BUILD_DEBIAN)
+
+
+qt5_use_modules(inputairspyhf Core Widgets)
+
+install(TARGETS inputairspyhf DESTINATION lib/plugins/samplesource)
diff --git a/plugins/samplesource/airspyhf/airspy.pro b/plugins/samplesource/airspyhf/airspy.pro
new file mode 100644
index 000000000..c31ae2e71
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspy.pro
@@ -0,0 +1,50 @@
+#--------------------------------------------------------
+#
+# Pro file for Android and Windows builds with Qt Creator
+#
+#--------------------------------------------------------
+
+TEMPLATE = lib
+CONFIG += plugin
+
+QT += core gui widgets multimedia opengl
+
+TARGET = inputairspyhf
+
+CONFIG(MINGW32):LIBAIRSPYHFSRC = "D:\softs\airspyhf"
+CONFIG(MINGW64):LIBAIRSPYHFSRC = "D:\softs\airspyhf"
+INCLUDEPATH += $$PWD
+INCLUDEPATH += ../../../sdrbase
+INCLUDEPATH += ../../../sdrgui
+INCLUDEPATH += ../../../swagger/sdrangel/code/qt5/client
+INCLUDEPATH += $$LIBAIRSPYHFSRC
+
+DEFINES += USE_SSE2=1
+QMAKE_CXXFLAGS += -msse2
+DEFINES += USE_SSE4_1=1
+QMAKE_CXXFLAGS += -msse4.1
+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
+
+HEADERS += airspyhfgui.h\
+ airspyhfinput.h\
+ airspyhfplugin.h\
+ airspyhfsettings.h\
+ airspyhfthread.h
+
+FORMS += airspyhfgui.ui
+
+LIBS += -L../../../sdrbase/$${build_subdir} -lsdrbase
+LIBS += -L../../../sdrgui/$${build_subdir} -lsdrgui
+LIBS += -L../../../swagger/$${build_subdir} -lswagger
+LIBS += -L../../../libairspyhf/$${build_subdir} -llibairspyhf
+
+RESOURCES = ../../../sdrgui/resources/res.qrc
diff --git a/plugins/samplesource/airspyhf/airspyhfgui.cpp b/plugins/samplesource/airspyhf/airspyhfgui.cpp
new file mode 100644
index 000000000..ac35c31c0
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfgui.cpp
@@ -0,0 +1,395 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "airspyhfgui.h"
+
+#include
+#include "device/deviceuiset.h"
+#include
+
+#include "ui_airspyhfgui.h"
+#include "gui/colormapper.h"
+#include "gui/glspectrum.h"
+#include "dsp/dspengine.h"
+#include "dsp/dspcommands.h"
+
+AirspyHFGui::AirspyHFGui(DeviceUISet *deviceUISet, QWidget* parent) :
+ QWidget(parent),
+ ui(new Ui::AirspyHFGui),
+ m_deviceUISet(deviceUISet),
+ m_doApplySettings(true),
+ m_forceSettings(true),
+ m_settings(),
+ m_sampleSource(0),
+ m_lastEngineState((DSPDeviceSourceEngine::State)-1)
+{
+ m_sampleSource = (AirspyHFInput*) 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 = ((AirspyHFInput*) m_sampleSource)->getSampleRates();
+ displaySampleRates();
+ connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()), Qt::QueuedConnection);
+
+ sendSettings();
+}
+
+AirspyHFGui::~AirspyHFGui()
+{
+ delete ui;
+}
+
+void AirspyHFGui::destroy()
+{
+ delete this;
+}
+
+void AirspyHFGui::setName(const QString& name)
+{
+ setObjectName(name);
+}
+
+QString AirspyHFGui::getName() const
+{
+ return objectName();
+}
+
+void AirspyHFGui::resetToDefaults()
+{
+ m_settings.resetToDefaults();
+ displaySettings();
+ sendSettings();
+}
+
+qint64 AirspyHFGui::getCenterFrequency() const
+{
+ return m_settings.m_centerFrequency;
+}
+
+void AirspyHFGui::setCenterFrequency(qint64 centerFrequency)
+{
+ m_settings.m_centerFrequency = centerFrequency;
+ displaySettings();
+ sendSettings();
+}
+
+QByteArray AirspyHFGui::serialize() const
+{
+ return m_settings.serialize();
+}
+
+bool AirspyHFGui::deserialize(const QByteArray& data)
+{
+ if(m_settings.deserialize(data)) {
+ displaySettings();
+ m_forceSettings = true;
+ sendSettings();
+ return true;
+ } else {
+ resetToDefaults();
+ return false;
+ }
+}
+
+bool AirspyHFGui::handleMessage(const Message& message)
+{
+ if (AirspyHFInput::MsgConfigureAirspyHF::match(message))
+ {
+ const AirspyHFInput::MsgConfigureAirspyHF& cfg = (AirspyHFInput::MsgConfigureAirspyHF&) message;
+ m_settings = cfg.getSettings();
+ blockApplySettings(true);
+ displaySettings();
+ blockApplySettings(false);
+ return true;
+ }
+ else if (AirspyHFInput::MsgStartStop::match(message))
+ {
+ AirspyHFInput::MsgStartStop& notif = (AirspyHFInput::MsgStartStop&) message;
+ blockApplySettings(true);
+ ui->startStop->setChecked(notif.getStartStop());
+ blockApplySettings(false);
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void AirspyHFGui::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 AirspyHFGui::updateSampleRateAndFrequency()
+{
+ m_deviceUISet->getSpectrum()->setSampleRate(m_sampleRate);
+ m_deviceUISet->getSpectrum()->setCenterFrequency(m_deviceCenterFrequency);
+ ui->deviceRateText->setText(tr("%1k").arg((float)m_sampleRate / 1000));
+}
+
+void AirspyHFGui::updateFrequencyLimits()
+{
+ // values in kHz
+ qint64 deltaFrequency = m_settings.m_transverterMode ? m_settings.m_transverterDeltaFrequency/1000 : 0;
+ qint64 minLimit = AirspyHFInput::loLowLimitFreqHF/1000 + deltaFrequency;
+ qint64 maxLimit = AirspyHFInput::loHighLimitFreqHF/1000 + deltaFrequency;
+
+ 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 AirspyHFGui::displaySettings()
+{
+ ui->transverter->setDeltaFrequency(m_settings.m_transverterDeltaFrequency);
+ ui->transverter->setDeltaFrequencyActive(m_settings.m_transverterMode);
+ updateFrequencyLimits();
+ 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->dcOffset->setChecked(m_settings.m_dcBlock);
+ ui->iqImbalance->setChecked(m_settings.m_iqCorrection);
+
+ ui->decim->setCurrentIndex(m_settings.m_log2Decim);
+
+ ui->fcPos->setCurrentIndex((int) m_settings.m_fcPos);
+}
+
+void AirspyHFGui::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 AirspyHFGui::sendSettings()
+{
+ if(!m_updateTimer.isActive())
+ m_updateTimer.start(100);
+}
+
+void AirspyHFGui::on_centerFrequency_changed(quint64 value)
+{
+ m_settings.m_centerFrequency = value * 1000;
+ sendSettings();
+}
+
+void AirspyHFGui::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 AirspyHFGui::on_dcOffset_toggled(bool checked)
+{
+ m_settings.m_dcBlock = checked;
+ sendSettings();
+}
+
+void AirspyHFGui::on_iqImbalance_toggled(bool checked)
+{
+ m_settings.m_iqCorrection = checked;
+ sendSettings();
+}
+
+void AirspyHFGui::on_sampleRate_currentIndexChanged(int index)
+{
+ m_settings.m_devSampleRateIndex = index;
+ sendSettings();
+}
+
+void AirspyHFGui::on_decim_currentIndexChanged(int index)
+{
+ if ((index <0) || (index > 6))
+ return;
+ m_settings.m_log2Decim = index;
+ sendSettings();
+}
+
+void AirspyHFGui::on_fcPos_currentIndexChanged(int index)
+{
+ if (index == 0) {
+ m_settings.m_fcPos = AirspyHFSettings::FC_POS_INFRA;
+ sendSettings();
+ } else if (index == 1) {
+ m_settings.m_fcPos = AirspyHFSettings::FC_POS_SUPRA;
+ sendSettings();
+ } else if (index == 2) {
+ m_settings.m_fcPos = AirspyHFSettings::FC_POS_CENTER;
+ sendSettings();
+ }
+}
+
+void AirspyHFGui::on_startStop_toggled(bool checked)
+{
+ if (m_doApplySettings)
+ {
+ AirspyHFInput::MsgStartStop *message = AirspyHFInput::MsgStartStop::create(checked);
+ m_sampleSource->getInputMessageQueue()->push(message);
+ }
+}
+
+void AirspyHFGui::on_record_toggled(bool checked)
+{
+ if (checked) {
+ ui->record->setStyleSheet("QToolButton { background-color : red; }");
+ } else {
+ ui->record->setStyleSheet("QToolButton { background:rgb(79,79,79); }");
+ }
+
+ AirspyHFInput::MsgFileRecord* message = AirspyHFInput::MsgFileRecord::create(checked);
+ m_sampleSource->getInputMessageQueue()->push(message);
+}
+
+void AirspyHFGui::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 AirspyHFGui::updateHardware()
+{
+ qDebug() << "AirspyHFGui::updateHardware";
+ AirspyHFInput::MsgConfigureAirspyHF* message = AirspyHFInput::MsgConfigureAirspyHF::create(m_settings, m_forceSettings);
+ m_sampleSource->getInputMessageQueue()->push(message);
+ m_forceSettings = false;
+ m_updateTimer.stop();
+}
+
+void AirspyHFGui::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 AirspyHFGui::getDevSampleRate(unsigned int rate_index)
+{
+ if (rate_index < m_rates.size())
+ {
+ return m_rates[rate_index];
+ }
+ else
+ {
+ return m_rates[0];
+ }
+}
+
+int AirspyHFGui::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/airspyhf/airspyhfgui.h b/plugins/samplesource/airspyhf/airspyhfgui.h
new file mode 100644
index 000000000..6c935ddf8
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfgui.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_AIRSPYHFGUI_H
+#define INCLUDE_AIRSPYHFGUI_H
+
+#include
+#include
+#include
+
+#include "util/messagequeue.h"
+#include "airspyhfinput.h"
+
+class DeviceUISet;
+
+namespace Ui {
+ class AirspyHFGui;
+ class AirspyHFSampleRates;
+}
+
+class AirspyHFGui : public QWidget, public PluginInstanceGUI {
+ Q_OBJECT
+
+public:
+ explicit AirspyHFGui(DeviceUISet *deviceUISet, QWidget* parent = 0);
+ virtual ~AirspyHFGui();
+ 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::AirspyHFGui* ui;
+
+ DeviceUISet* m_deviceUISet;
+ bool m_doApplySettings;
+ bool m_forceSettings;
+ AirspyHFSettings 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_dcOffset_toggled(bool checked);
+ void on_iqImbalance_toggled(bool checked);
+ void on_sampleRate_currentIndexChanged(int index);
+ void on_decim_currentIndexChanged(int index);
+ void on_fcPos_currentIndexChanged(int index);
+ void on_startStop_toggled(bool checked);
+ void on_record_toggled(bool checked);
+ void on_transverter_clicked();
+ void updateHardware();
+ void updateStatus();
+ void handleInputMessages();
+};
+
+#endif // INCLUDE_AIRSPYHFGUI_H
diff --git a/plugins/samplesource/airspyhf/airspyhfgui.ui b/plugins/samplesource/airspyhf/airspyhfgui.ui
new file mode 100644
index 000000000..35e2098f8
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfgui.ui
@@ -0,0 +1,513 @@
+
+
+ AirspyHFGui
+
+
+
+ 0
+ 0
+ 300
+ 240
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 300
+ 220
+
+
+
+
+ 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
+
+
+
+
+
+
+ -
+
+
+ 3
+
+
-
+
+
+ LO correction ppm
+
+
+ LO ppm
+
+
+
+ -
+
+
+ Local Oscillator frequency correction (ppm)
+
+
+ -100
+
+
+ 100
+
+
+ 1
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 0.0
+
+
+
+
+
+ -
+
+
-
+
+
+ DC offset auto correction
+
+
+ DC
+
+
+
+ -
+
+
+ IQ imbalance auto correction
+
+
+ IQ
+
+
+
+ -
+
+
+ Auto
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 24
+ 24
+
+
+
+ Transverter frequency translation dialog
+
+
+ X
+
+
+
+ -
+
+
+
+ 50
+ 16777215
+
+
+
-
+
+ HF
+
+
+ -
+
+ VHF
+
+
+
+
+ -
+
+
+ Band
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Rate
+
+
+
+ -
+
+
+
+ 60
+ 0
+
+
+
+
+ 60
+ 16777215
+
+
+
+ Device sample rate in MS/s
+
+
-
+
+ 0000
+
+
+
+
+ -
+
+
+ Dec
+
+
+
+ -
+
+
+ k
+
+
+
+ -
+
+
+ Fp
+
+
+
+ -
+
+
+
+ 50
+ 16777215
+
+
+
+ Decimation factor
+
+
+ 3
+
+
-
+
+ 1
+
+
+ -
+
+ 2
+
+
+ -
+
+ 4
+
+
+ -
+
+ 8
+
+
+ -
+
+ 16
+
+
+ -
+
+ 32
+
+
+ -
+
+ 64
+
+
+
+
+ -
+
+
+
+ 55
+ 16777215
+
+
+
+ Relative position of device center frequency
+
+
+ 2
+
+
-
+
+ Inf
+
+
+ -
+
+ Sup
+
+
+ -
+
+ Cen
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+ ValueDial
+ QWidget
+
+ 1
+
+
+ ButtonSwitch
+ QToolButton
+
+
+
+ TransverterButton
+ QPushButton
+
+
+
+
+
+
+
+
diff --git a/plugins/samplesource/airspyhf/airspyhfinput.cpp b/plugins/samplesource/airspyhf/airspyhfinput.cpp
new file mode 100644
index 000000000..b024c0e48
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfinput.cpp
@@ -0,0 +1,500 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "airspyhfgui.h"
+#include "airspyhfinput.h"
+#include "airspyhfplugin.h"
+
+#include
+#include
+#include "dsp/dspcommands.h"
+#include "dsp/dspengine.h"
+#include "airspyhfsettings.h"
+#include "airspyhfthread.h"
+
+MESSAGE_CLASS_DEFINITION(AirspyHFInput::MsgConfigureAirspyHF, Message)
+MESSAGE_CLASS_DEFINITION(AirspyHFInput::MsgStartStop, Message)
+MESSAGE_CLASS_DEFINITION(AirspyHFInput::MsgFileRecord, Message)
+
+const qint64 AirspyHFInput::loLowLimitFreqHF = 9000L;
+const qint64 AirspyHFInput::loHighLimitFreqHF = 31000000L;
+const qint64 AirspyHFInput::loLowLimitFreqVHF = 60000000L;
+const qint64 AirspyHFInput::loHighLimitFreqVHF = 260000000L;
+
+AirspyHFInput::AirspyHFInput(DeviceSourceAPI *deviceAPI) :
+ m_deviceAPI(deviceAPI),
+ m_settings(),
+ m_dev(0),
+ m_airspyHFThread(0),
+ m_deviceDescription("AirspyHF"),
+ 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);
+}
+
+AirspyHFInput::~AirspyHFInput()
+{
+ if (m_running) { stop(); }
+ m_deviceAPI->removeSink(m_fileSink);
+ delete m_fileSink;
+ closeDevice();
+}
+
+void AirspyHFInput::destroy()
+{
+ delete this;
+}
+
+bool AirspyHFInput::openDevice()
+{
+ if (m_dev != 0)
+ {
+ closeDevice();
+ }
+
+ airspyhf_error rc;
+
+ if (!m_sampleFifo.setSize(1<<19))
+ {
+ qCritical("AirspyHFInput::start: could not allocate SampleFifo");
+ return false;
+ }
+
+ if ((m_dev = open_airspyhf_from_serial(m_deviceAPI->getSampleSourceSerial())) == 0)
+ {
+ qCritical("AirspyHFInput::start: could not open Airspy with serial %s", qPrintable(m_deviceAPI->getSampleSourceSerial()));
+ return false;
+ }
+ else
+ {
+ qDebug("AirspyHFInput::start: opened Airspy 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("AirspyHFInput::start: %d sample rates for AirspyHF", nbSampleRates);
+ }
+ else
+ {
+ qCritical("AirspyHFInput::start: could not obtain the number of AirspyHF sample rates");
+ return false;
+ }
+
+ sampleRates = new uint32_t[nbSampleRates];
+
+ rc = (airspyhf_error) airspyhf_get_samplerates(m_dev, sampleRates, nbSampleRates);
+
+ if (rc == AIRSPYHF_SUCCESS)
+ {
+ qDebug("AirspyHFInput::start: obtained AirspyHF sample rates");
+ }
+ else
+ {
+ qCritical("AirspyHFInput::start: could not obtain AirspyHF sample rates");
+ return false;
+ }
+
+ m_sampleRates.clear();
+
+ for (unsigned int i = 0; i < nbSampleRates; i++)
+ {
+ m_sampleRates.push_back(sampleRates[i]);
+ qDebug("AirspyHFInput::start: sampleRates[%d] = %u Hz", i, sampleRates[i]);
+ }
+
+ delete[] sampleRates;
+
+ return true;
+}
+
+void AirspyHFInput::init()
+{
+ applySettings(m_settings, true);
+}
+
+bool AirspyHFInput::start()
+{
+ QMutexLocker mutexLocker(&m_mutex);
+
+ if (!m_dev) {
+ return false;
+ }
+
+ if (m_running) { stop(); }
+
+ if ((m_airspyHFThread = new AirspyHFThread(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->setFcPos((int) m_settings.m_fcPos);
+
+ m_airspyHFThread->startWork();
+
+ mutexLocker.unlock();
+
+ applySettings(m_settings, true);
+
+ qDebug("AirspyHFInput::startInput: started");
+ m_running = true;
+
+ return true;
+}
+
+void AirspyHFInput::closeDevice()
+{
+ if (m_dev != 0)
+ {
+ airspyhf_stop(m_dev);
+ airspyhf_close(m_dev);
+ m_dev = 0;
+ }
+
+ m_deviceDescription.clear();
+}
+
+void AirspyHFInput::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 AirspyHFInput::serialize() const
+{
+ return m_settings.serialize();
+}
+
+bool AirspyHFInput::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& AirspyHFInput::getDeviceDescription() const
+{
+ return m_deviceDescription;
+}
+
+int AirspyHFInput::getSampleRate() const
+{
+ int rate = m_sampleRates[m_settings.m_devSampleRateIndex];
+ return (rate / (1<push(messageToGUI);
+ }
+}
+
+bool AirspyHFInput::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 AirspyHFInput::setDeviceCenterFrequency(quint64 freq_hz)
+{
+ qint64 df = ((qint64)freq_hz * m_settings.m_LOppmTenths) / 10000000LL;
+ freq_hz += df;
+
+ airspyhf_error rc = (airspyhf_error) airspyhf_set_freq(m_dev, static_cast(freq_hz));
+
+ if (rc == AIRSPYHF_SUCCESS) {
+ qDebug("AirspyInput::setDeviceCenterFrequency: frequency set to %llu Hz", freq_hz);
+ } else {
+ qWarning("AirspyInput::setDeviceCenterFrequency: could not frequency to %llu Hz", freq_hz);
+ }
+}
+
+bool AirspyHFInput::applySettings(const AirspyHFSettings& 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_dcBlock != settings.m_dcBlock) ||
+ (m_settings.m_iqCorrection != settings.m_iqCorrection) || force)
+ {
+ m_deviceAPI->configureCorrections(settings.m_dcBlock, settings.m_iqCorrection);
+ }
+
+ 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<setFcPos((int) settings.m_fcPos);
+ qDebug() << "AirspyInput: set fc pos (enum) to " << (int) settings.m_fcPos;
+ }
+ }
+
+ if (forwardChange)
+ {
+ int sampleRate = m_sampleRates[sampleRateIndex]/(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 *AirspyHFInput::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 AirspyHFInput::webapiRunGet(
+ SWGSDRangel::SWGDeviceState& response,
+ QString& errorMessage __attribute__((unused)))
+{
+ m_deviceAPI->getDeviceEngineStateStr(*response.getState());
+ return 200;
+}
+
+int AirspyHFInput::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/airspyhf/airspyhfinput.h b/plugins/samplesource/airspyhf/airspyhfinput.h
new file mode 100644
index 000000000..730093463
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfinput.h
@@ -0,0 +1,146 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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_AIRSPYHFINPUT_H
+#define INCLUDE_AIRSPYHFINPUT_H
+
+#include
+#include
+
+#include
+#include
+#include "airspyhfsettings.h"
+
+class DeviceSourceAPI;
+class AirspyHFThread;
+class FileRecord;
+
+class AirspyHFInput : public DeviceSampleSource {
+public:
+ class MsgConfigureAirspyHF : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ const AirspyHFSettings& getSettings() const { return m_settings; }
+ bool getForce() const { return m_force; }
+
+ static MsgConfigureAirspyHF* create(const AirspyHFSettings& settings, bool force)
+ {
+ return new MsgConfigureAirspyHF(settings, force);
+ }
+
+ private:
+ AirspyHFSettings m_settings;
+ bool m_force;
+
+ MsgConfigureAirspyHF(const AirspyHFSettings& 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)
+ { }
+ };
+
+ AirspyHFInput(DeviceSourceAPI *deviceAPI);
+ virtual ~AirspyHFInput();
+ 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 AirspyHFSettings& settings, bool force);
+ airspyhf_device_t *open_airspyhf_from_serial(const QString& serialStr);
+ void setDeviceCenterFrequency(quint64 freq);
+
+ DeviceSourceAPI *m_deviceAPI;
+ QMutex m_mutex;
+ AirspyHFSettings m_settings;
+ airspyhf_device_t* m_dev;
+ AirspyHFThread* 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_AIRSPYHFINPUT_H
diff --git a/plugins/samplesource/airspyhf/airspyhfplugin.cpp b/plugins/samplesource/airspyhf/airspyhfplugin.cpp
new file mode 100644
index 000000000..16bc6815c
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfplugin.cpp
@@ -0,0 +1,127 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "airspyhfgui.h"
+#include "airspyhfplugin.h"
+
+#include
+#include "plugin/pluginapi.h"
+#include "util/simpleserializer.h"
+
+
+const PluginDescriptor AirspyHFPlugin::m_pluginDescriptor = {
+ QString("AirspyHF Input"),
+ QString("3.11.0"),
+ QString("(c) Edouard Griffiths, F4EXB"),
+ QString("https://github.com/f4exb/sdrangel"),
+ true,
+ QString("https://github.com/f4exb/sdrangel")
+};
+
+const QString AirspyHFPlugin::m_hardwareID = "AirspyHF";
+const QString AirspyHFPlugin::m_deviceTypeID = AIRSPYHF_DEVICE_TYPE_ID;
+const int AirspyHFPlugin::m_maxDevices = 32;
+
+AirspyHFPlugin::AirspyHFPlugin(QObject* parent) :
+ QObject(parent)
+{
+}
+
+const PluginDescriptor& AirspyHFPlugin::getPluginDescriptor() const
+{
+ return m_pluginDescriptor;
+}
+
+void AirspyHFPlugin::initPlugin(PluginAPI* pluginAPI)
+{
+ pluginAPI->registerSampleSource(m_deviceTypeID, this);
+}
+
+PluginInterface::SamplingDevices AirspyHFPlugin::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[%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("AirspyPlugin::enumSampleSources: enumerated Airspy device #%d", i);
+ }
+ else
+ {
+ qDebug("AirspyHFPlugin::enumSampleSources: finished to enumerate Airspy HF. %d devices found", i);
+ break; // finished
+ }
+ }
+
+ return result;
+}
+
+PluginInstanceGUI* AirspyHFPlugin::createSampleSourcePluginInstanceGUI(
+ const QString& sourceId,
+ QWidget **widget,
+ DeviceUISet *deviceUISet)
+{
+ if (sourceId == m_deviceTypeID)
+ {
+ AirspyHFGui* gui = new AirspyHFGui(deviceUISet);
+ *widget = gui;
+ return gui;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+DeviceSampleSource *AirspyHFPlugin::createSampleSourcePluginInstanceInput(const QString& sourceId, DeviceSourceAPI *deviceAPI)
+{
+ if (sourceId == m_deviceTypeID)
+ {
+ AirspyHFInput* input = new AirspyHFInput(deviceAPI);
+ return input;
+ }
+ else
+ {
+ return 0;
+ }
+}
diff --git a/plugins/samplesource/airspyhf/airspyhfplugin.h b/plugins/samplesource/airspyhf/airspyhfplugin.h
new file mode 100644
index 000000000..3bc6b4a43
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfplugin.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_AIRSPYHFPLUGIN_H
+#define INCLUDE_AIRSPYHFPLUGIN_H
+
+#include
+#include "plugin/plugininterface.h"
+
+#define AIRSPYHF_DEVICE_TYPE_ID "sdrangel.samplesource.airspyhf"
+
+class PluginAPI;
+
+class AirspyHFPlugin : public QObject, public PluginInterface {
+ Q_OBJECT
+ Q_INTERFACES(PluginInterface)
+ Q_PLUGIN_METADATA(IID AIRSPYHF_DEVICE_TYPE_ID)
+
+public:
+ explicit AirspyHFPlugin(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_AIRSPYPLUGIN_H
diff --git a/plugins/samplesource/airspyhf/airspyhfsettings.cpp b/plugins/samplesource/airspyhf/airspyhfsettings.cpp
new file mode 100644
index 000000000..ea36f62b8
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfsettings.cpp
@@ -0,0 +1,86 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "airspyhfsettings.h"
+
+AirspyHFSettings::AirspyHFSettings()
+{
+ resetToDefaults();
+}
+
+void AirspyHFSettings::resetToDefaults()
+{
+ m_centerFrequency = 7150*1000;
+ m_devSampleRateIndex = 0;
+ m_LOppmTenths = 0;
+ m_log2Decim = 0;
+ m_fcPos = FC_POS_CENTER;
+ m_dcBlock = false;
+ m_iqCorrection = false;
+ m_transverterMode = false;
+ m_transverterDeltaFrequency = 0;
+}
+
+QByteArray AirspyHFSettings::serialize() const
+{
+ SimpleSerializer s(1);
+
+ s.writeU32(1, m_devSampleRateIndex);
+ s.writeS32(2, m_LOppmTenths);
+ s.writeU32(3, m_log2Decim);
+ s.writeS32(4, m_fcPos);
+ s.writeBool(5, m_dcBlock);
+ s.writeBool(6, m_iqCorrection);
+ s.writeBool(7, m_transverterMode);
+ s.writeS64(8, m_transverterDeltaFrequency);
+
+ return s.final();
+}
+
+bool AirspyHFSettings::deserialize(const QByteArray& data)
+{
+ SimpleDeserializer d(data);
+
+ if (!d.isValid())
+ {
+ resetToDefaults();
+ return false;
+ }
+
+ if (d.getVersion() == 1)
+ {
+ int intval;
+
+ d.readU32(1, &m_devSampleRateIndex, 0);
+ d.readS32(2, &m_LOppmTenths, 0);
+ d.readU32(3, &m_log2Decim, 0);
+ d.readS32(4, &intval, 0);
+ m_fcPos = (fcPos_t) intval;
+ d.readBool(5, &m_dcBlock, false);
+ d.readBool(6, &m_iqCorrection, false);
+ d.readBool(7, &m_transverterMode, false);
+ d.readS64(8, &m_transverterDeltaFrequency, 0);
+
+ return true;
+ }
+ else
+ {
+ resetToDefaults();
+ return false;
+ }
+}
diff --git a/plugins/samplesource/airspyhf/airspyhfsettings.h b/plugins/samplesource/airspyhf/airspyhfsettings.h
new file mode 100644
index 000000000..5936c4deb
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfsettings.h
@@ -0,0 +1,43 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 _AIRSPY_AIRSPYHFSETTINGS_H_
+#define _AIRSPY_AIRSPYHFSETTINGS_H_
+
+struct AirspyHFSettings {
+ typedef enum {
+ FC_POS_INFRA = 0,
+ FC_POS_SUPRA,
+ FC_POS_CENTER
+ } fcPos_t;
+
+ quint64 m_centerFrequency;
+ quint32 m_devSampleRateIndex;
+ qint32 m_LOppmTenths;
+ quint32 m_log2Decim;
+ fcPos_t m_fcPos;
+ bool m_dcBlock;
+ bool m_iqCorrection;
+ bool m_transverterMode;
+ qint64 m_transverterDeltaFrequency;
+
+ AirspyHFSettings();
+ void resetToDefaults();
+ QByteArray serialize() const;
+ bool deserialize(const QByteArray& data);
+};
+
+#endif /* _AIRSPY_AIRSPYHFSETTINGS_H_ */
diff --git a/plugins/samplesource/airspyhf/airspyhfthread.cpp b/plugins/samplesource/airspyhf/airspyhfthread.cpp
new file mode 100644
index 000000000..d35c0a7a0
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfthread.cpp
@@ -0,0 +1,208 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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 "airspyhfthread.h"
+
+#include "dsp/samplesinkfifo.h"
+
+AirspyHFThread *AirspyHFThread::m_this = 0;
+
+AirspyHFThread::AirspyHFThread(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, QObject* parent) :
+ QThread(parent),
+ m_running(false),
+ m_dev(dev),
+ m_convertBuffer(AIRSPYHF_BLOCKSIZE),
+ m_sampleFifo(sampleFifo),
+ m_samplerate(10),
+ m_log2Decim(0),
+ m_fcPos(0)
+{
+ m_this = this;
+}
+
+AirspyHFThread::~AirspyHFThread()
+{
+ stopWork();
+ m_this = 0;
+}
+
+void AirspyHFThread::startWork()
+{
+ m_startWaitMutex.lock();
+ start();
+ while(!m_running)
+ m_startWaiter.wait(&m_startWaitMutex, 100);
+ m_startWaitMutex.unlock();
+}
+
+void AirspyHFThread::stopWork()
+{
+ qDebug("AirspyThread::stopWork");
+ m_running = false;
+ wait();
+}
+
+void AirspyHFThread::setSamplerate(uint32_t samplerate)
+{
+ m_samplerate = samplerate;
+}
+
+void AirspyHFThread::setLog2Decimation(unsigned int log2_decim)
+{
+ m_log2Decim = log2_decim;
+}
+
+void AirspyHFThread::setFcPos(int fcPos)
+{
+ m_fcPos = fcPos;
+}
+
+void AirspyHFThread::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("AirspyHFThread::run: failed to start Airspy 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("AirspyHFThread::run: stopped Airspy Rx");
+ } else {
+ qDebug("AirspyHFThread::run: failed to stop Airspy Rx");
+ }
+
+ m_running = false;
+}
+
+// Decimate according to specified log2 (ex: log2=4 => decim=16)
+void AirspyHFThread::callback(const qint16* buf, qint32 len)
+{
+ SampleVector::iterator it = m_convertBuffer.begin();
+
+ if (m_log2Decim == 0)
+ {
+ m_decimators.decimate1(&it, buf, len);
+ }
+ else
+ {
+ if (m_fcPos == 0) // Infra
+ {
+ switch (m_log2Decim)
+ {
+ case 1:
+ m_decimators.decimate2_inf(&it, buf, len);
+ break;
+ case 2:
+ m_decimators.decimate4_inf(&it, buf, len);
+ break;
+ case 3:
+ m_decimators.decimate8_inf(&it, buf, len);
+ break;
+ case 4:
+ m_decimators.decimate16_inf(&it, buf, len);
+ break;
+ case 5:
+ m_decimators.decimate32_inf(&it, buf, len);
+ break;
+ case 6:
+ m_decimators.decimate64_inf(&it, buf, len);
+ break;
+ default:
+ break;
+ }
+ }
+ else if (m_fcPos == 1) // Supra
+ {
+ switch (m_log2Decim)
+ {
+ case 1:
+ m_decimators.decimate2_sup(&it, buf, len);
+ break;
+ case 2:
+ m_decimators.decimate4_sup(&it, buf, len);
+ break;
+ case 3:
+ m_decimators.decimate8_sup(&it, buf, len);
+ break;
+ case 4:
+ m_decimators.decimate16_sup(&it, buf, len);
+ break;
+ case 5:
+ m_decimators.decimate32_sup(&it, buf, len);
+ break;
+ case 6:
+ m_decimators.decimate64_sup(&it, buf, len);
+ break;
+ default:
+ break;
+ }
+ }
+ else if (m_fcPos == 2) // Center
+ {
+ switch (m_log2Decim)
+ {
+ 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 AirspyHFThread::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/airspyhf/airspyhfthread.h b/plugins/samplesource/airspyhf/airspyhfthread.h
new file mode 100644
index 000000000..63e78e9c9
--- /dev/null
+++ b/plugins/samplesource/airspyhf/airspyhfthread.h
@@ -0,0 +1,65 @@
+///////////////////////////////////////////////////////////////////////////////////
+// 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_AIRSPYHFTHREAD_H
+#define INCLUDE_AIRSPYHFTHREAD_H
+
+#include
+#include
+#include
+#include
+
+#include "dsp/samplesinkfifo.h"
+#include "dsp/decimators.h"
+
+#define AIRSPYHF_BLOCKSIZE (1<<17)
+
+class AirspyHFThread : public QThread {
+ Q_OBJECT
+
+public:
+ AirspyHFThread(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, QObject* parent = 0);
+ ~AirspyHFThread();
+
+ void startWork();
+ void stopWork();
+ void setSamplerate(uint32_t samplerate);
+ void setLog2Decimation(unsigned int log2_decim);
+ void setFcPos(int fcPos);
+
+private:
+ QMutex m_startWaitMutex;
+ QWaitCondition m_startWaiter;
+ bool m_running;
+
+ airspyhf_device_t* m_dev;
+ qint16 m_buf[2*AIRSPYHF_BLOCKSIZE];
+ SampleVector m_convertBuffer;
+ SampleSinkFifo* m_sampleFifo;
+
+ int m_samplerate;
+ unsigned int m_log2Decim;
+ int m_fcPos;
+ static AirspyHFThread *m_this;
+
+ Decimators m_decimators;
+
+ void run();
+ void callback(const qint16* buf, qint32 len);
+ static int rx_callback(airspyhf_transfer_t* transfer);
+};
+
+#endif // INCLUDE_AIRSPYHFTHREAD_H
diff --git a/sdrangel.windows.pro b/sdrangel.windows.pro
index b4f5f7e7e..2d7a2daf7 100644
--- a/sdrangel.windows.pro
+++ b/sdrangel.windows.pro
@@ -20,6 +20,7 @@ SUBDIRS += fcdlib
SUBDIRS += librtlsdr
SUBDIRS += libhackrf
SUBDIRS += libairspy
+SUBDIRS += libairspyhf
SUBDIRS += libbladerf
SUBDIRS += libsqlite3
SUBDIRS += liblimesuite
@@ -33,6 +34,7 @@ CONFIG(MINGW64)SUBDIRS += plugins/samplesource/sdrdaemonsource
SUBDIRS += plugins/samplesource/rtlsdr
SUBDIRS += plugins/samplesource/hackrfinput
SUBDIRS += plugins/samplesource/airspy
+SUBDIRS += plugins/samplesource/airspyhf
SUBDIRS += plugins/samplesource/bladerfinput
SUBDIRS += plugins/samplesource/limesdrinput
SUBDIRS += plugins/samplesource/plutosdrinput
diff --git a/udev-rules/52-airspyhf.rules b/udev-rules/52-airspyhf.rules
new file mode 100644
index 000000000..d7dee4578
--- /dev/null
+++ b/udev-rules/52-airspyhf.rules
@@ -0,0 +1 @@
+ATTR{idVendor}=="03eb", ATTR{idProduct}=="800c", SYMLINK+="airspyhf-%k", MODE="660", GROUP="plugdev"
diff --git a/udev-rules/install.sh b/udev-rules/install.sh
index 1164a0d23..b4533faec 100755
--- a/udev-rules/install.sh
+++ b/udev-rules/install.sh
@@ -1,6 +1,7 @@
#!/bin/sh
cp 52-airspy.rules /etc/udev/rules.d/
+cp 52-airspyhf.rules /etc/udev/rules.d/
cp 88-nuand.rules /etc/udev/rules.d/
cp fcd.rules /etc/udev/rules.d/
cp 53-hackrf.rules /etc/udev/rules.d/