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 +
gui/valuedial.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + TransverterButton + QPushButton +
gui/transverterbutton.h
+
+
+ + + + +
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/