mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-06-02 14:04:46 -04:00
1285 lines
51 KiB
C++
1285 lines
51 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// SDRangel Fobos SDR native source backend
|
|
// Auto backend selection: Agile API first, regular/classic API fallback.
|
|
// Runtime DLLs are loaded dynamically so SDRangel can still build without the Fobos SDK.
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "fobosworker.h"
|
|
|
|
#include <QDebug>
|
|
#include <QDateTime>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <chrono>
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
#if defined(FOBOS_DEBUG_FILE_LOG)
|
|
static const char* kLogPath = "sdrangel_fobos_source.log";
|
|
#endif
|
|
static const char* kAgileDll = "fobos_sdr.dll";
|
|
static const char* kRegularDll = "fobos.dll";
|
|
static const uint32_t kSyncComplexBufferLength = 65536; // Sync read block size, about 8.2 ms at 8 MS/s
|
|
static const uint32_t kSyncFloatStorage = kSyncComplexBufferLength * 2u; // interleaved float I/Q
|
|
static const uint32_t kFifoChunkComplexLength = kSyncComplexBufferLength; // One FIFO write per read block
|
|
static const unsigned int kStartupProbeMaxAttempts = 8; // Verify read_sync before exposing a running stream
|
|
static const unsigned int kStartupReaderErrorAbort = 32; // avoid endless dev==NULL read loop after a failed start
|
|
static const double kDefaultFrequencyHz = 104000000.0;
|
|
static const int kDefaultSampleRateHz = 25000000;
|
|
static const unsigned int kDefaultLna = 2;
|
|
static const unsigned int kDefaultVga = 8;
|
|
static const double kDefaultIqGain = 32.0;
|
|
static const int kDefaultInputMode = 0; // RF
|
|
static const unsigned int kDefaultBandwidthPercent = 90;
|
|
static const unsigned int kDefaultGpoMask = 0;
|
|
static const bool kDefaultExternalClock = false;
|
|
|
|
#ifdef _WIN32
|
|
static QMutex g_sessionMutex;
|
|
static HMODULE g_agileLibrary = nullptr; // DLL is process-scoped; device is not.
|
|
static HMODULE g_regularLibrary = nullptr; // regular/classic DLL, process-scoped as well.
|
|
static bool g_deviceBusy = false; // prevent two SDRangel device sets from using one Fobos.
|
|
|
|
static void formatLastError(DWORD err, char* out, size_t outSize)
|
|
{
|
|
if (!out || outSize == 0) {
|
|
return;
|
|
}
|
|
out[0] = '\0';
|
|
DWORD n = FormatMessageA(
|
|
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr,
|
|
err,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
out,
|
|
static_cast<DWORD>(outSize),
|
|
nullptr);
|
|
if (n == 0) {
|
|
std::snprintf(out, outSize, "FormatMessage failed");
|
|
} else {
|
|
while (n > 0 && (out[n-1] == '\r' || out[n-1] == '\n' || out[n-1] == ' ' || out[n-1] == '\t')) {
|
|
out[n-1] = '\0';
|
|
--n;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
FOBOSWorker::FOBOSWorker(SampleSinkFifo* sampleFifo, QObject* parent) :
|
|
QObject(parent),
|
|
m_sampleFifo(sampleFifo),
|
|
m_convertBuffer(kSyncComplexBufferLength),
|
|
m_centerFrequencyHz(static_cast<uint64_t>(kDefaultFrequencyHz)),
|
|
m_samplerate(kDefaultSampleRateHz),
|
|
m_log2Decim(0),
|
|
m_fcPos(0),
|
|
m_frequencyShift(0),
|
|
m_lnaGain(kDefaultLna),
|
|
m_vgaGain(kDefaultVga),
|
|
m_inputMode(kDefaultInputMode),
|
|
m_bandwidthPercent(kDefaultBandwidthPercent),
|
|
m_gpoMask(kDefaultGpoMask),
|
|
m_externalClock(kDefaultExternalClock),
|
|
m_iqGain(kDefaultIqGain),
|
|
m_runtimeInputMode(kDefaultInputMode),
|
|
m_gainDiagActive(false),
|
|
m_gainDiagCollecting(false),
|
|
m_gainDiagKind(),
|
|
m_gainDiagValue(0),
|
|
m_gainDiagSkipBuffers(0),
|
|
m_gainDiagTargetBuffers(0),
|
|
m_gainDiagCollectedBuffers(0),
|
|
m_gainDiagSamples(0),
|
|
m_gainDiagPower(0.0),
|
|
m_gainDiagPeak(0.0),
|
|
m_running(false),
|
|
m_stopRequested(false),
|
|
m_syncStarted(false),
|
|
m_readerActive(false),
|
|
m_readerFinished(false),
|
|
m_dev(nullptr),
|
|
m_runtimeBackend(FobosRuntimeBackend::None)
|
|
#ifdef _WIN32
|
|
, m_libraryHandle(nullptr),
|
|
m_errorName(nullptr),
|
|
m_closeDev(nullptr),
|
|
m_readSync(nullptr),
|
|
m_stopSync(nullptr),
|
|
m_setFrequency(nullptr),
|
|
m_setSamplerate(nullptr),
|
|
m_getSamplerates(nullptr),
|
|
m_setDirectSampling(nullptr),
|
|
m_setAutoBandwidth(nullptr),
|
|
m_setUserGpo(nullptr),
|
|
m_setClkSource(nullptr),
|
|
m_setLnaGain(nullptr),
|
|
m_setVgaGain(nullptr),
|
|
m_regularSetFrequency(nullptr),
|
|
m_regularSetSamplerate(nullptr),
|
|
m_regularGetSamplerates(nullptr),
|
|
m_regularSetDirectSampling(nullptr),
|
|
m_regularSetUserGpo(nullptr),
|
|
m_regularSetClkSource(nullptr),
|
|
m_regularSetLnaGain(nullptr),
|
|
m_regularSetVgaGain(nullptr)
|
|
#endif
|
|
, m_totalReads(0),
|
|
m_totalSamples(0),
|
|
m_totalWritten(0),
|
|
m_failedReads(0)
|
|
{
|
|
}
|
|
|
|
FOBOSWorker::~FOBOSWorker()
|
|
{
|
|
stopWork();
|
|
}
|
|
|
|
void FOBOSWorker::startWork()
|
|
{
|
|
bool expected = false;
|
|
if (!m_running.compare_exchange_strong(expected, true)) {
|
|
return;
|
|
}
|
|
|
|
qInfo() << "FOBOSWorker::startWork: Fobos SDR sync source starting";
|
|
emit backendStatusChanged(QStringLiteral("Probing"), QStringLiteral("Backend mode: Auto. Trying Agile API first, then regular API."));
|
|
if (runAgileStart()) {
|
|
qInfo() << "FOBOSWorker::startWork: Agile sync source started";
|
|
return;
|
|
}
|
|
|
|
if (!m_stopRequested.load()) {
|
|
m_running.store(true);
|
|
qInfo() << "FOBOSWorker::startWork: Agile not available, trying regular Fobos API";
|
|
if (runRegularStart()) {
|
|
qInfo() << "FOBOSWorker::startWork: Regular sync source started";
|
|
return;
|
|
}
|
|
}
|
|
|
|
qInfo() << "FOBOSWorker::startWork: no usable Fobos backend started";
|
|
emit backendStatusChanged(QStringLiteral("No device"), QStringLiteral("No Fobos SDR device was found through Agile or regular API."));
|
|
}
|
|
|
|
void FOBOSWorker::stopWork()
|
|
{
|
|
emit backendStatusChanged(QStringLiteral("Stopped"), QStringLiteral("Fobos source is stopped. Backend will be detected on next Start."));
|
|
m_stopRequested.store(true);
|
|
m_running.store(false);
|
|
|
|
#ifdef _WIN32
|
|
bool readerJoined = true;
|
|
|
|
// Do not call stop_sync while read_sync is active.
|
|
// The reader loop checks m_stopRequested after every successful read_sync and exits by itself.
|
|
// stop_sync is called only after the reader thread has returned. This avoids driver/API
|
|
// races between a blocking read_sync call and stop_sync.
|
|
if (m_readerThread.joinable()) {
|
|
logLine("reader_join=START strategy=stop_flag_then_join_before_stop_sync");
|
|
m_readerThread.join();
|
|
readerJoined = true;
|
|
logLine("reader_join=RETURN joined=YES before_stop_sync");
|
|
}
|
|
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (dev && m_stopSync && m_syncStarted.exchange(false)) {
|
|
logLine("stopWork_stop_sync=START_AFTER_READER_JOIN");
|
|
int r = m_stopSync(dev);
|
|
logLine("stopWork_stop_sync=RETURN_AFTER_READER_JOIN result=%d error='%s'", r, errorName(r));
|
|
} else {
|
|
logLine("stopWork_stop_sync=SKIPPED no_dev_or_not_started");
|
|
}
|
|
|
|
cleanupAfterReaderJoined(readerJoined);
|
|
#endif
|
|
}
|
|
|
|
void FOBOSWorker::setSamplerate(int samplerate)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_samplerate = samplerate > 0 ? samplerate : kDefaultSampleRateHz;
|
|
if (m_running.load()) {
|
|
logLine("live_sample_rate_change=CONTROLLED_RESTART_REQUIRED_BUT_NOT_HANDLED_HERE requested_hz=%d", m_samplerate);
|
|
}
|
|
}
|
|
|
|
void FOBOSWorker::setCenterFrequency(uint64_t centerFrequencyHz)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_centerFrequencyHz = centerFrequencyHz > 0 ? centerFrequencyHz : static_cast<uint64_t>(kDefaultFrequencyHz);
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (dev && m_runtimeInputMode.load() == 0) {
|
|
int r = callSetFrequency(dev, static_cast<double>(m_centerFrequencyHz));
|
|
logLine("live_frequency_call=RETURN result=%d error='%s' requested_hz=%llu", r, errorName(r), static_cast<unsigned long long>(m_centerFrequencyHz));
|
|
} else if (m_running.load()) {
|
|
logLine("live_frequency_call=SKIPPED mode=%d no_dev_or_direct_sampling", m_runtimeInputMode.load());
|
|
}
|
|
}
|
|
|
|
void FOBOSWorker::setLog2Decimation(unsigned int log2_decim)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_log2Decim = log2_decim;
|
|
}
|
|
|
|
void FOBOSWorker::setFcPos(int fcPos)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_fcPos = fcPos;
|
|
}
|
|
|
|
void FOBOSWorker::setBitSize(uint32_t) {}
|
|
void FOBOSWorker::setAmplitudeBits(int32_t amplitudeBits)
|
|
{
|
|
double gain = static_cast<double>(amplitudeBits) / 100.0;
|
|
if (gain < 0.01) {
|
|
gain = 0.01;
|
|
} else if (gain > 512.0) {
|
|
gain = 512.0;
|
|
}
|
|
m_iqGain.store(gain);
|
|
if (m_running.load()) { logLine("live_iq_gain_set=%.3f", gain); }
|
|
}
|
|
void FOBOSWorker::setLnaGain(unsigned int lnaGain)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_lnaGain = lnaGain > 2u ? 2u : lnaGain;
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (dev) {
|
|
int r = callSetLnaGain(dev, m_lnaGain);
|
|
logLine("live_lna_gain_call=RETURN result=%d error='%s' value=%u", r, errorName(r), m_lnaGain);
|
|
}
|
|
}
|
|
void FOBOSWorker::setVgaGain(unsigned int vgaGain)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_vgaGain = vgaGain > 31u ? 31u : vgaGain;
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (dev) {
|
|
int r = callSetVgaGain(dev, m_vgaGain);
|
|
logLine("live_vga_gain_call=RETURN result=%d error='%s' value=%u", r, errorName(r), m_vgaGain);
|
|
}
|
|
}
|
|
void FOBOSWorker::setInputMode(int inputMode)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
if (inputMode < 0 || inputMode > 3) {
|
|
inputMode = kDefaultInputMode;
|
|
}
|
|
m_inputMode = inputMode;
|
|
if (m_running.load()) {
|
|
logLine("live_input_mode_change=CONTROLLED_RESTART_REQUIRED_BUT_NOT_HANDLED_HERE input_mode=%d", inputMode);
|
|
}
|
|
}
|
|
|
|
void FOBOSWorker::setBandwidthPercent(unsigned int bandwidthPercent)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
if (!(bandwidthPercent >= 20 && bandwidthPercent <= 100 && (bandwidthPercent % 10) == 0)) {
|
|
bandwidthPercent = kDefaultBandwidthPercent;
|
|
}
|
|
m_bandwidthPercent = bandwidthPercent;
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (dev) {
|
|
int r = callSetAutoBandwidth(dev, m_bandwidthPercent);
|
|
logLine("live_auto_bandwidth_call=RETURN result=%d error='%s' percent=%u", r, errorName(r), m_bandwidthPercent);
|
|
}
|
|
}
|
|
|
|
void FOBOSWorker::setGpoMask(unsigned int gpoMask)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_gpoMask = gpoMask & 0xffu;
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (dev) {
|
|
int r = callSetGpo(dev, m_gpoMask);
|
|
logLine("live_gpo_call=RETURN result=%d error='%s' mask=0x%02X", r, errorName(r), m_gpoMask);
|
|
}
|
|
}
|
|
|
|
void FOBOSWorker::setExternalClock(bool externalClock)
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
m_externalClock = externalClock;
|
|
if (m_running.load()) {
|
|
logLine("live_external_clock_change=CONTROLLED_RESTART_REQUIRED_BUT_NOT_HANDLED_HERE external_clock=%s", externalClock ? "true" : "false");
|
|
}
|
|
}
|
|
|
|
void FOBOSWorker::setDCFactor(float) {}
|
|
void FOBOSWorker::setIFactor(float) {}
|
|
void FOBOSWorker::setQFactor(float) {}
|
|
void FOBOSWorker::setPhaseImbalance(float) {}
|
|
void FOBOSWorker::setFrequencyShift(int shift) { QMutexLocker locker(&m_settingsMutex); m_frequencyShift = shift; }
|
|
void FOBOSWorker::setToneFrequency(int) {}
|
|
void FOBOSWorker::setModulation(int modulation)
|
|
{
|
|
// Legacy modulation combo is now the uSDR-style VGA gain control.
|
|
// Direct range is 0..31.
|
|
if (modulation < 0) {
|
|
modulation = 0;
|
|
} else if (modulation > 31) {
|
|
modulation = 31;
|
|
}
|
|
setVgaGain(static_cast<unsigned int>(modulation));
|
|
}
|
|
void FOBOSWorker::setAMModulation(float) {}
|
|
void FOBOSWorker::setFMDeviation(float) {}
|
|
void FOBOSWorker::setPattern0() {}
|
|
void FOBOSWorker::setPattern1() {}
|
|
void FOBOSWorker::setPattern2() {}
|
|
|
|
|
|
|
|
bool FOBOSWorker::runAgileStart()
|
|
{
|
|
#ifdef _WIN32
|
|
logLine("============================================================");
|
|
logLine("SDRangel Fobos SDR Agile native source");
|
|
logTimestamp();
|
|
logLine("mode=agile_sync_to_sdrangel_fifo");
|
|
logLine("lifecycle_policy=stopflag_join_then_stopsync_close");
|
|
logLine("stream_policy=stable_fast_float_to_fix_single_write_no_decimation");
|
|
logLine("agile_dll=%s", kAgileDll);
|
|
|
|
uint64_t centerFrequency = 0;
|
|
int sampleRate = 0;
|
|
unsigned int lnaGain = kDefaultLna;
|
|
unsigned int vgaGain = kDefaultVga;
|
|
int inputMode = kDefaultInputMode;
|
|
unsigned int bandwidthPercent = kDefaultBandwidthPercent;
|
|
unsigned int gpoMask = kDefaultGpoMask;
|
|
bool externalClock = kDefaultExternalClock;
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
centerFrequency = m_centerFrequencyHz;
|
|
sampleRate = m_samplerate > 0 ? m_samplerate : kDefaultSampleRateHz;
|
|
lnaGain = m_lnaGain;
|
|
vgaGain = m_vgaGain;
|
|
inputMode = m_inputMode;
|
|
bandwidthPercent = m_bandwidthPercent;
|
|
gpoMask = m_gpoMask;
|
|
externalClock = m_externalClock;
|
|
}
|
|
|
|
logLine("requested_center_frequency_hz=%llu", static_cast<unsigned long long>(centerFrequency));
|
|
logLine("requested_sample_rate_hz=%d", sampleRate);
|
|
logLine("requested_lna=%u", lnaGain);
|
|
logLine("requested_vga=%u", vgaGain);
|
|
logLine("requested_iq_gain=%.3f", m_iqGain.load());
|
|
logLine("requested_input_mode=%d", inputMode);
|
|
logLine("requested_bandwidth_percent=%u", bandwidthPercent);
|
|
logLine("requested_gpo_mask=0x%02X", gpoMask);
|
|
logLine("requested_external_clock=%s", externalClock ? "true" : "false");
|
|
HMODULE lib = nullptr;
|
|
|
|
{
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
|
|
if (g_deviceBusy) {
|
|
logLine("device_busy_guard=ACTIVE another_FOBOS_instance_is_already_streaming");
|
|
logLine("source_result=REFUSED_DEVICE_ALREADY_BUSY");
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
|
|
if (!g_agileLibrary) {
|
|
// Resolve fobos_sdr.dll using the normal Windows DLL search order.
|
|
// No developer-machine paths are used here; distribution packages should place
|
|
// fobos_sdr.dll and its runtime dependencies next to the SDRangel executable,
|
|
// or the user may expose them through PATH.
|
|
SetLastError(0);
|
|
g_agileLibrary = LoadLibraryA(kAgileDll);
|
|
DWORD gle = GetLastError();
|
|
logLine("LoadLibrary=%s handle=0x%p gle=%lu", g_agileLibrary ? "OK" : "FAILED", g_agileLibrary, gle);
|
|
if (!g_agileLibrary) {
|
|
char errText[512] = {};
|
|
formatLastError(gle, errText, sizeof(errText));
|
|
logLine("LoadLibrary_error='%s'", errText);
|
|
logLine("operator_action=install_fobos_sdr_dll_next_to_sdrangel_or_add_it_to_PATH_then_restart_SDRangel");
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
} else {
|
|
logLine("LoadLibrary=REUSE_PROCESS_SCOPED handle=0x%p", g_agileLibrary);
|
|
}
|
|
|
|
lib = g_agileLibrary;
|
|
g_deviceBusy = true;
|
|
}
|
|
|
|
m_libraryHandle = lib;
|
|
m_runtimeBackend = FobosRuntimeBackend::Agile;
|
|
|
|
auto getApiInfo = reinterpret_cast<fobos_sdr_get_api_info_t>(GetProcAddress(lib, "fobos_sdr_get_api_info"));
|
|
auto getDeviceCount = reinterpret_cast<fobos_sdr_get_device_count_t>(GetProcAddress(lib, "fobos_sdr_get_device_count"));
|
|
auto listDevices = reinterpret_cast<fobos_sdr_list_devices_t>(GetProcAddress(lib, "fobos_sdr_list_devices"));
|
|
auto openDev = reinterpret_cast<fobos_sdr_open_t>(GetProcAddress(lib, "fobos_sdr_open"));
|
|
m_closeDev = reinterpret_cast<fobos_sdr_close_t>(GetProcAddress(lib, "fobos_sdr_close"));
|
|
auto getBoardInfo = reinterpret_cast<fobos_sdr_get_board_info_t>(GetProcAddress(lib, "fobos_sdr_get_board_info"));
|
|
m_errorName = reinterpret_cast<fobos_sdr_error_name_t>(GetProcAddress(lib, "fobos_sdr_error_name"));
|
|
m_setFrequency = reinterpret_cast<fobos_sdr_set_frequency_t>(GetProcAddress(lib, "fobos_sdr_set_frequency"));
|
|
auto setFrequency = m_setFrequency;
|
|
m_setSamplerate = reinterpret_cast<fobos_sdr_set_samplerate_t>(GetProcAddress(lib, "fobos_sdr_set_samplerate"));
|
|
m_getSamplerates = reinterpret_cast<fobos_sdr_get_samplerates_t>(GetProcAddress(lib, "fobos_sdr_get_samplerates"));
|
|
m_setDirectSampling = reinterpret_cast<fobos_sdr_set_direct_sampling_t>(GetProcAddress(lib, "fobos_sdr_set_direct_sampling"));
|
|
m_setAutoBandwidth = reinterpret_cast<fobos_sdr_set_auto_bandwidth_t>(GetProcAddress(lib, "fobos_sdr_set_auto_bandwidth"));
|
|
m_setUserGpo = reinterpret_cast<fobos_sdr_set_user_gpo_t>(GetProcAddress(lib, "fobos_sdr_set_user_gpo"));
|
|
m_setClkSource = reinterpret_cast<fobos_sdr_set_clk_source_t>(GetProcAddress(lib, "fobos_sdr_set_clk_source"));
|
|
auto setSamplerate = m_setSamplerate;
|
|
m_setLnaGain = reinterpret_cast<fobos_sdr_set_lna_gain_t>(GetProcAddress(lib, "fobos_sdr_set_lna_gain"));
|
|
m_setVgaGain = reinterpret_cast<fobos_sdr_set_vga_gain_t>(GetProcAddress(lib, "fobos_sdr_set_vga_gain"));
|
|
auto setLnaGain = m_setLnaGain;
|
|
auto setVgaGain = m_setVgaGain;
|
|
auto startSync = reinterpret_cast<fobos_sdr_start_sync_t>(GetProcAddress(lib, "fobos_sdr_start_sync"));
|
|
m_readSync = reinterpret_cast<fobos_sdr_read_sync_t>(GetProcAddress(lib, "fobos_sdr_read_sync"));
|
|
m_stopSync = reinterpret_cast<fobos_sdr_stop_sync_t>(GetProcAddress(lib, "fobos_sdr_stop_sync"));
|
|
|
|
bool ok = getApiInfo && getDeviceCount && listDevices && openDev && m_closeDev && getBoardInfo && m_errorName && setFrequency && setSamplerate && setLnaGain && setVgaGain && startSync && m_readSync && m_stopSync;
|
|
logLine("exports_ok=%s", ok ? "YES" : "NO");
|
|
if (!ok) {
|
|
logLine("source_result=FAILED_MISSING_AGILE_EXPORTS");
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
|
|
char libVersion[128] = {};
|
|
char drvVersion[128] = {};
|
|
int rInfo = getApiInfo(libVersion, drvVersion);
|
|
logLine("api_info_call=result=%d lib='%s' drv='%s'", rInfo, libVersion, drvVersion);
|
|
|
|
int count = getDeviceCount();
|
|
logLine("device_count_call=OK count=%d", count);
|
|
if (count <= 0) {
|
|
logLine("device_detect_hint=NO_FOBOS_DEVICE_VISIBLE_TO_AGILE_API");
|
|
logLine("device_busy_hint=check_USB_connection_and_close_microSDR_uSDR_other_SDRangel_instances");
|
|
logLine("source_result=FAILED_NO_DEVICE_VISIBLE");
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
|
|
char serials[4096] = {};
|
|
int rList = listDevices(serials);
|
|
logLine("list_devices_call=RETURN result=%d serials='%s'", rList, serials);
|
|
|
|
fobos_dev_t* dev = nullptr;
|
|
int r = openDev(&dev, 0);
|
|
logLine("open_call=RETURN result=%d error='%s' dev=0x%p", r, errorName(r), dev);
|
|
if ((r != 0) || !dev) {
|
|
logLine("device_open_hint=FAILED_TO_OPEN_FOBOS_INDEX_0");
|
|
logLine("device_busy_hint=Fobos_may_be_owned_by_microSDR_uSDR_or_another_SDRangel_instance");
|
|
logLine("operator_action=close_other_Fobos_software_then_Stop_Start_or_restart_SDRangel");
|
|
logLine("source_result=FAILED_OPEN_DEVICE_BUSY_OR_UNAVAILABLE");
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
m_dev.store(dev);
|
|
|
|
char hw[128] = {}, fw[128] = {}, manufacturer[128] = {}, product[128] = {}, serial[128] = {};
|
|
int rBoard = getBoardInfo(dev, hw, fw, manufacturer, product, serial);
|
|
logLine("board_info_call=RETURN result=%d error='%s'", rBoard, errorName(rBoard));
|
|
logLine("board_hw_revision='%s'", hw);
|
|
logLine("board_fw_version='%s'", fw);
|
|
logLine("board_manufacturer='%s'", manufacturer);
|
|
logLine("board_product='%s'", product);
|
|
logLine("board_serial='%s'", serial);
|
|
const QString agileBackendDetails = QStringLiteral("Fobos Agile API %1; driver %2; HW %3; FW %4; serial %5")
|
|
.arg(QString::fromLocal8Bit(libVersion))
|
|
.arg(QString::fromLocal8Bit(drvVersion))
|
|
.arg(QString::fromLocal8Bit(hw))
|
|
.arg(QString::fromLocal8Bit(fw))
|
|
.arg(QString::fromLocal8Bit(serial));
|
|
|
|
if (m_getSamplerates) {
|
|
double rates[32] = {};
|
|
uint32_t rateCount = 32;
|
|
int rRates = m_getSamplerates(dev, rates, &rateCount);
|
|
logLine("get_samplerates_call=RETURN result=%d count=%u", rRates, rateCount);
|
|
for (uint32_t i = 0; (rRates == 0) && (i < rateCount) && (i < 32); ++i) {
|
|
logLine("supported_samplerate[%u]=%.0f", i, rates[i]);
|
|
}
|
|
} else {
|
|
logLine("get_samplerates_call=SKIPPED missing_export fallback_list=8000000,10000000,12500000,16000000,20000000,25000000,32000000,40000000,50000000");
|
|
}
|
|
|
|
const int direct = (inputMode == 0) ? 0 : 1;
|
|
if (m_setDirectSampling) {
|
|
r = m_setDirectSampling(dev, direct);
|
|
logLine("set_direct_sampling_call=RETURN result=%d error='%s' input_mode=%d direct=%d", r, errorName(r), inputMode, direct);
|
|
} else {
|
|
logLine("set_direct_sampling_call=SKIPPED missing_export input_mode=%d direct=%d", inputMode, direct);
|
|
}
|
|
m_runtimeInputMode.store(inputMode);
|
|
|
|
if (m_setClkSource) {
|
|
r = m_setClkSource(dev, externalClock ? 1 : 0);
|
|
logLine("set_clk_source_call=RETURN result=%d error='%s' external_clock=%s", r, errorName(r), externalClock ? "true" : "false");
|
|
} else {
|
|
logLine("set_clk_source_call=SKIPPED missing_export external_clock=%s", externalClock ? "true" : "false");
|
|
}
|
|
|
|
if (m_setUserGpo) {
|
|
r = m_setUserGpo(dev, gpoMask);
|
|
logLine("set_user_gpo_call=RETURN result=%d error='%s' mask=0x%02X", r, errorName(r), gpoMask);
|
|
} else {
|
|
logLine("set_user_gpo_call=SKIPPED missing_export mask=0x%02X", gpoMask);
|
|
}
|
|
|
|
if (m_setAutoBandwidth) {
|
|
const double bwRatio = static_cast<double>(bandwidthPercent) * 0.01;
|
|
r = m_setAutoBandwidth(dev, bwRatio);
|
|
logLine("set_auto_bandwidth_call=RETURN result=%d error='%s' percent=%u ratio=%.3f", r, errorName(r), bandwidthPercent, bwRatio);
|
|
} else {
|
|
logLine("set_auto_bandwidth_call=SKIPPED missing_export percent=%u", bandwidthPercent);
|
|
}
|
|
|
|
r = callSetFrequency(dev, static_cast<double>(centerFrequency));
|
|
logLine("set_frequency_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
r = callSetSamplerate(dev, static_cast<double>(sampleRate));
|
|
logLine("set_samplerate_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
r = callSetLnaGain(dev, lnaGain);
|
|
logLine("set_lna_gain_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
r = callSetVgaGain(dev, vgaGain);
|
|
logLine("set_vga_gain_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
|
|
r = startSync(dev, kSyncComplexBufferLength);
|
|
logLine("start_sync_call=RETURN result=%d error='%s' complex_buf_length=%u float_storage=%u fifo_write_complex=%u", r, errorName(r), kSyncComplexBufferLength, kSyncFloatStorage, kFifoChunkComplexLength);
|
|
if (r != 0) {
|
|
m_dev.store(nullptr);
|
|
m_running.store(false);
|
|
if (m_closeDev) {
|
|
int rc = m_closeDev(dev);
|
|
logLine("close_after_start_sync_failure=RETURN result=%d error='%s'", rc, errorName(rc));
|
|
}
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
return false;
|
|
}
|
|
|
|
// Start/read robustness:
|
|
// Some driver/device states can report a successful start while the first read_sync calls
|
|
// still fail, for example with a null device handle reported by the runtime. Do not expose
|
|
// such a half-started source as Running.
|
|
// The first read_sync loop may return repeated errors and
|
|
// no samples are delivered. Probe read_sync synchronously, close/release cleanly if the device handle is unusable,
|
|
// and let the operator Start again without flooding the log or leaving a stuck channel.
|
|
{
|
|
std::vector<float> startupProbe(kSyncFloatStorage);
|
|
bool startupProbeOk = false;
|
|
uint32_t startupProbeActual = 0;
|
|
int startupProbeResult = -9999;
|
|
|
|
for (unsigned int attempt = 1; attempt <= kStartupProbeMaxAttempts && !m_stopRequested.load(); ++attempt) {
|
|
startupProbeActual = 0;
|
|
startupProbeResult = m_readSync(dev, startupProbe.data(), &startupProbeActual);
|
|
if ((startupProbeResult == 0) && (startupProbeActual > 0)) {
|
|
startupProbeOk = true;
|
|
logLine("startup_read_probe=OK attempt=%u actual=%u", attempt, startupProbeActual);
|
|
break;
|
|
}
|
|
logLine("startup_read_probe=RETRY attempt=%u result=%d error='%s' actual=%u",
|
|
attempt, startupProbeResult, errorName(startupProbeResult), startupProbeActual);
|
|
Sleep(20);
|
|
}
|
|
|
|
if (!startupProbeOk) {
|
|
logLine("startup_read_probe=FAILED max_attempts=%u last_result=%d last_error='%s' action=stop_sync_close_release_busy",
|
|
kStartupProbeMaxAttempts, startupProbeResult, errorName(startupProbeResult));
|
|
if (m_stopSync) {
|
|
int rs = m_stopSync(dev);
|
|
logLine("stop_sync_after_startup_probe_failure=RETURN result=%d error='%s'", rs, errorName(rs));
|
|
}
|
|
if (m_closeDev) {
|
|
int rc = m_closeDev(dev);
|
|
logLine("close_after_startup_probe_failure=RETURN result=%d error='%s'", rc, errorName(rc));
|
|
}
|
|
m_dev.store(nullptr);
|
|
m_syncStarted.store(false);
|
|
m_running.store(false);
|
|
m_stopRequested.store(false);
|
|
{
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
}
|
|
logLine("source_result=FAILED_STARTUP_READ_PROBE_NO_SAMPLES_DEVICE_RELEASED");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_syncStarted.store(true);
|
|
m_readerFinished.store(false);
|
|
m_readerActive.store(true);
|
|
m_stopRequested.store(false);
|
|
m_totalReads = 0;
|
|
m_totalSamples = 0;
|
|
m_totalWritten = 0;
|
|
m_failedReads = 0;
|
|
|
|
logLine("reader_thread=STARTING");
|
|
m_readerThread = std::thread(&FOBOSWorker::readerLoop, this);
|
|
logLine("reader_thread=STARTED");
|
|
emit backendStatusChanged(QStringLiteral("Agile"), agileBackendDetails);
|
|
return true;
|
|
#else
|
|
logLine("Fobos SDR Agile dynamic runtime backend is currently implemented for Windows only");
|
|
m_running.store(false);
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
|
|
bool FOBOSWorker::runRegularStart()
|
|
{
|
|
#ifdef _WIN32
|
|
logLine("============================================================");
|
|
logLine("SDRangel Fobos SDR regular native source");
|
|
logTimestamp();
|
|
logLine("mode=regular_sync_to_sdrangel_fifo");
|
|
logLine("regular_dll=%s", kRegularDll);
|
|
|
|
uint64_t centerFrequency = 0;
|
|
int sampleRate = 0;
|
|
unsigned int lnaGain = kDefaultLna;
|
|
unsigned int vgaGain = kDefaultVga;
|
|
int inputMode = kDefaultInputMode;
|
|
unsigned int bandwidthPercent = kDefaultBandwidthPercent;
|
|
unsigned int gpoMask = kDefaultGpoMask;
|
|
bool externalClock = kDefaultExternalClock;
|
|
{
|
|
QMutexLocker locker(&m_settingsMutex);
|
|
centerFrequency = m_centerFrequencyHz;
|
|
sampleRate = m_samplerate > 0 ? m_samplerate : kDefaultSampleRateHz;
|
|
lnaGain = m_lnaGain;
|
|
vgaGain = m_vgaGain;
|
|
inputMode = m_inputMode;
|
|
bandwidthPercent = m_bandwidthPercent;
|
|
gpoMask = m_gpoMask;
|
|
externalClock = m_externalClock;
|
|
}
|
|
|
|
HMODULE lib = nullptr;
|
|
{
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
if (g_deviceBusy) {
|
|
logLine("regular_device_busy_guard=ACTIVE another_FOBOS_instance_is_already_streaming");
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
if (!g_regularLibrary) {
|
|
SetLastError(0);
|
|
g_regularLibrary = LoadLibraryA(kRegularDll);
|
|
DWORD gle = GetLastError();
|
|
logLine("regular_LoadLibrary=%s handle=0x%p gle=%lu", g_regularLibrary ? "OK" : "FAILED", g_regularLibrary, gle);
|
|
if (!g_regularLibrary) {
|
|
char errText[512] = {};
|
|
formatLastError(gle, errText, sizeof(errText));
|
|
logLine("regular_LoadLibrary_error='%s'", errText);
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
} else {
|
|
logLine("regular_LoadLibrary=REUSE_PROCESS_SCOPED handle=0x%p", g_regularLibrary);
|
|
}
|
|
lib = g_regularLibrary;
|
|
g_deviceBusy = true;
|
|
}
|
|
|
|
m_libraryHandle = lib;
|
|
m_runtimeBackend = FobosRuntimeBackend::Regular;
|
|
|
|
auto getApiInfo = reinterpret_cast<fobos_rx_get_api_info_t>(GetProcAddress(lib, "fobos_rx_get_api_info"));
|
|
auto getDeviceCount = reinterpret_cast<fobos_rx_get_device_count_t>(GetProcAddress(lib, "fobos_rx_get_device_count"));
|
|
auto listDevices = reinterpret_cast<fobos_rx_list_devices_t>(GetProcAddress(lib, "fobos_rx_list_devices"));
|
|
auto openDev = reinterpret_cast<fobos_rx_open_t>(GetProcAddress(lib, "fobos_rx_open"));
|
|
m_closeDev = reinterpret_cast<fobos_sdr_close_t>(GetProcAddress(lib, "fobos_rx_close"));
|
|
auto getBoardInfo = reinterpret_cast<fobos_sdr_get_board_info_t>(GetProcAddress(lib, "fobos_rx_get_board_info"));
|
|
m_errorName = reinterpret_cast<fobos_sdr_error_name_t>(GetProcAddress(lib, "fobos_rx_error_name"));
|
|
m_regularSetFrequency = reinterpret_cast<fobos_rx_set_frequency_t>(GetProcAddress(lib, "fobos_rx_set_frequency"));
|
|
m_regularSetSamplerate = reinterpret_cast<fobos_rx_set_samplerate_t>(GetProcAddress(lib, "fobos_rx_set_samplerate"));
|
|
m_regularGetSamplerates = reinterpret_cast<fobos_rx_get_samplerates_t>(GetProcAddress(lib, "fobos_rx_get_samplerates"));
|
|
m_regularSetDirectSampling = reinterpret_cast<fobos_rx_set_direct_sampling_t>(GetProcAddress(lib, "fobos_rx_set_direct_sampling"));
|
|
m_regularSetUserGpo = reinterpret_cast<fobos_rx_set_user_gpo_t>(GetProcAddress(lib, "fobos_rx_set_user_gpo"));
|
|
m_regularSetClkSource = reinterpret_cast<fobos_rx_set_clk_source_t>(GetProcAddress(lib, "fobos_rx_set_clk_source"));
|
|
m_regularSetLnaGain = reinterpret_cast<fobos_rx_set_lna_gain_t>(GetProcAddress(lib, "fobos_rx_set_lna_gain"));
|
|
m_regularSetVgaGain = reinterpret_cast<fobos_rx_set_vga_gain_t>(GetProcAddress(lib, "fobos_rx_set_vga_gain"));
|
|
auto startSync = reinterpret_cast<fobos_rx_start_sync_t>(GetProcAddress(lib, "fobos_rx_start_sync"));
|
|
m_readSync = reinterpret_cast<fobos_sdr_read_sync_t>(GetProcAddress(lib, "fobos_rx_read_sync"));
|
|
m_stopSync = reinterpret_cast<fobos_sdr_stop_sync_t>(GetProcAddress(lib, "fobos_rx_stop_sync"));
|
|
|
|
const bool ok = getApiInfo && getDeviceCount && listDevices && openDev && m_closeDev && getBoardInfo && m_errorName &&
|
|
m_regularSetFrequency && m_regularSetSamplerate && m_regularSetLnaGain && m_regularSetVgaGain &&
|
|
startSync && m_readSync && m_stopSync;
|
|
logLine("regular_exports_ok=%s", ok ? "YES" : "NO");
|
|
if (!ok) {
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
|
|
char libVersion[128] = {};
|
|
char drvVersion[128] = {};
|
|
int rInfo = getApiInfo(libVersion, drvVersion);
|
|
logLine("regular_api_info_call=result=%d lib='%s' drv='%s'", rInfo, libVersion, drvVersion);
|
|
|
|
int count = getDeviceCount();
|
|
logLine("regular_device_count_call=count=%d", count);
|
|
if (count <= 0) {
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
|
|
char serials[4096] = {};
|
|
int rList = listDevices(serials);
|
|
logLine("regular_list_devices_call=RETURN result=%d serials='%s'", rList, serials);
|
|
|
|
fobos_dev_t* dev = nullptr;
|
|
int r = openDev(&dev, 0);
|
|
logLine("regular_open_call=RETURN result=%d error='%s' dev=0x%p", r, errorName(r), dev);
|
|
if ((r != 0) || !dev) {
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
m_running.store(false);
|
|
return false;
|
|
}
|
|
m_dev.store(dev);
|
|
|
|
char hw[128] = {}, fw[128] = {}, manufacturer[128] = {}, product[128] = {}, serial[128] = {};
|
|
int rBoard = getBoardInfo(dev, hw, fw, manufacturer, product, serial);
|
|
logLine("regular_board_info_call=RETURN result=%d error='%s'", rBoard, errorName(rBoard));
|
|
logLine("regular_board_hw_revision='%s'", hw);
|
|
logLine("regular_board_fw_version='%s'", fw);
|
|
logLine("regular_board_manufacturer='%s'", manufacturer);
|
|
logLine("regular_board_product='%s'", product);
|
|
logLine("regular_board_serial='%s'", serial);
|
|
const QString regularBackendDetails = QStringLiteral("Fobos regular API %1; driver %2; HW %3; FW %4; serial %5")
|
|
.arg(QString::fromLocal8Bit(libVersion))
|
|
.arg(QString::fromLocal8Bit(drvVersion))
|
|
.arg(QString::fromLocal8Bit(hw))
|
|
.arg(QString::fromLocal8Bit(fw))
|
|
.arg(QString::fromLocal8Bit(serial));
|
|
|
|
if (m_regularGetSamplerates) {
|
|
double rates[32] = {};
|
|
unsigned int rateCount = 32;
|
|
int rRates = m_regularGetSamplerates(dev, rates, &rateCount);
|
|
logLine("regular_get_samplerates_call=RETURN result=%d count=%u", rRates, rateCount);
|
|
for (unsigned int i = 0; (rRates == 0) && (i < rateCount) && (i < 32); ++i) {
|
|
logLine("regular_supported_samplerate[%u]=%.0f", i, rates[i]);
|
|
}
|
|
}
|
|
|
|
r = callSetDirectSampling(dev, inputMode);
|
|
logLine("regular_set_direct_sampling_call=RETURN result=%d error='%s' input_mode=%d", r, errorName(r), inputMode);
|
|
m_runtimeInputMode.store(inputMode);
|
|
r = callSetClockSource(dev, externalClock);
|
|
logLine("regular_set_clk_source_call=RETURN result=%d error='%s' external_clock=%s", r, errorName(r), externalClock ? "true" : "false");
|
|
r = callSetGpo(dev, gpoMask);
|
|
logLine("regular_set_user_gpo_call=RETURN result=%d error='%s' mask=0x%02X", r, errorName(r), gpoMask);
|
|
r = callSetAutoBandwidth(dev, bandwidthPercent);
|
|
logLine("regular_set_auto_bandwidth_call=SKIPPED result=%d percent=%u", r, bandwidthPercent);
|
|
r = callSetFrequency(dev, static_cast<double>(centerFrequency));
|
|
logLine("regular_set_frequency_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
r = callSetSamplerate(dev, static_cast<double>(sampleRate));
|
|
logLine("regular_set_samplerate_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
r = callSetLnaGain(dev, lnaGain);
|
|
logLine("regular_set_lna_gain_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
r = callSetVgaGain(dev, vgaGain);
|
|
logLine("regular_set_vga_gain_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
|
|
r = startSync(dev, kSyncComplexBufferLength);
|
|
logLine("regular_start_sync_call=RETURN result=%d error='%s' complex_buf_length=%u", r, errorName(r), kSyncComplexBufferLength);
|
|
if (r != 0) {
|
|
m_dev.store(nullptr);
|
|
m_running.store(false);
|
|
if (m_closeDev) {
|
|
int rc = m_closeDev(dev);
|
|
logLine("regular_close_after_start_sync_failure=RETURN result=%d error='%s'", rc, errorName(rc));
|
|
}
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
return false;
|
|
}
|
|
|
|
{
|
|
std::vector<float> startupProbe(kSyncFloatStorage);
|
|
bool startupProbeOk = false;
|
|
uint32_t startupProbeActual = 0;
|
|
int startupProbeResult = -9999;
|
|
for (unsigned int attempt = 1; attempt <= kStartupProbeMaxAttempts && !m_stopRequested.load(); ++attempt) {
|
|
startupProbeActual = 0;
|
|
startupProbeResult = m_readSync(dev, startupProbe.data(), &startupProbeActual);
|
|
if ((startupProbeResult == 0) && (startupProbeActual > 0)) {
|
|
startupProbeOk = true;
|
|
logLine("regular_startup_read_probe=OK attempt=%u actual=%u", attempt, startupProbeActual);
|
|
break;
|
|
}
|
|
logLine("regular_startup_read_probe=RETRY attempt=%u result=%d error='%s' actual=%u",
|
|
attempt, startupProbeResult, errorName(startupProbeResult), startupProbeActual);
|
|
Sleep(20);
|
|
}
|
|
if (!startupProbeOk) {
|
|
if (m_stopSync) {
|
|
int rs = m_stopSync(dev);
|
|
logLine("regular_stop_sync_after_startup_probe_failure=RETURN result=%d error='%s'", rs, errorName(rs));
|
|
}
|
|
if (m_closeDev) {
|
|
int rc = m_closeDev(dev);
|
|
logLine("regular_close_after_startup_probe_failure=RETURN result=%d error='%s'", rc, errorName(rc));
|
|
}
|
|
m_dev.store(nullptr);
|
|
m_syncStarted.store(false);
|
|
m_running.store(false);
|
|
m_stopRequested.store(false);
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_syncStarted.store(true);
|
|
m_readerFinished.store(false);
|
|
m_readerActive.store(true);
|
|
m_stopRequested.store(false);
|
|
m_totalReads = 0;
|
|
m_totalSamples = 0;
|
|
m_totalWritten = 0;
|
|
m_failedReads = 0;
|
|
|
|
logLine("regular_reader_thread=STARTING");
|
|
m_readerThread = std::thread(&FOBOSWorker::readerLoop, this);
|
|
logLine("regular_reader_thread=STARTED");
|
|
emit backendStatusChanged(QStringLiteral("Regular"), regularBackendDetails);
|
|
return true;
|
|
#else
|
|
m_running.store(false);
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
int FOBOSWorker::callSetFrequency(fobos_dev_t* dev, double valueHz)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetFrequency) {
|
|
double actual = 0.0;
|
|
int r = m_regularSetFrequency(dev, valueHz, &actual);
|
|
logLine("regular_frequency_actual_hz=%.0f", actual);
|
|
return r;
|
|
}
|
|
if (m_setFrequency) {
|
|
return m_setFrequency(dev, valueHz);
|
|
}
|
|
#endif
|
|
return -9999;
|
|
}
|
|
|
|
int FOBOSWorker::callSetSamplerate(fobos_dev_t* dev, double valueHz)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetSamplerate)
|
|
{
|
|
double actual = 0.0;
|
|
return m_regularSetSamplerate(dev, valueHz, &actual);
|
|
}
|
|
|
|
if (m_setSamplerate)
|
|
{
|
|
return m_setSamplerate(dev, valueHz);
|
|
}
|
|
#endif
|
|
return -9999;
|
|
}
|
|
|
|
int FOBOSWorker::callSetDirectSampling(fobos_dev_t* dev, int inputMode)
|
|
{
|
|
#ifdef _WIN32
|
|
const int direct = (inputMode == 0) ? 0 : 1;
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetDirectSampling) {
|
|
return m_regularSetDirectSampling(dev, static_cast<unsigned int>(direct));
|
|
}
|
|
if (m_setDirectSampling) {
|
|
return m_setDirectSampling(dev, direct);
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int FOBOSWorker::callSetAutoBandwidth(fobos_dev_t* dev, unsigned int bandwidthPercent)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular) {
|
|
(void) dev;
|
|
(void) bandwidthPercent;
|
|
return 0; // regular libfobos has no auto-bandwidth setter
|
|
}
|
|
if (m_setAutoBandwidth) {
|
|
const double ratio = static_cast<double>(bandwidthPercent) * 0.01;
|
|
return m_setAutoBandwidth(dev, ratio);
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int FOBOSWorker::callSetGpo(fobos_dev_t* dev, unsigned int gpoMask)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetUserGpo) {
|
|
return m_regularSetUserGpo(dev, static_cast<uint8_t>(gpoMask & 0xffu));
|
|
}
|
|
if (m_setUserGpo) {
|
|
return m_setUserGpo(dev, gpoMask & 0xffu);
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int FOBOSWorker::callSetClockSource(fobos_dev_t* dev, bool externalClock)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetClkSource) {
|
|
return m_regularSetClkSource(dev, externalClock ? 1 : 0);
|
|
}
|
|
if (m_setClkSource) {
|
|
return m_setClkSource(dev, externalClock ? 1 : 0);
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int FOBOSWorker::callSetLnaGain(fobos_dev_t* dev, unsigned int lnaGain)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetLnaGain) {
|
|
return m_regularSetLnaGain(dev, lnaGain);
|
|
}
|
|
if (m_setLnaGain) {
|
|
return m_setLnaGain(dev, lnaGain);
|
|
}
|
|
#endif
|
|
return -9999;
|
|
}
|
|
|
|
int FOBOSWorker::callSetVgaGain(fobos_dev_t* dev, unsigned int vgaGain)
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_runtimeBackend == FobosRuntimeBackend::Regular && m_regularSetVgaGain) {
|
|
return m_regularSetVgaGain(dev, vgaGain);
|
|
}
|
|
if (m_setVgaGain) {
|
|
return m_setVgaGain(dev, vgaGain);
|
|
}
|
|
#endif
|
|
return -9999;
|
|
}
|
|
|
|
void FOBOSWorker::readerLoop()
|
|
{
|
|
#ifdef _WIN32
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
|
logLine("reader_thread_priority=THREAD_PRIORITY_HIGHEST");
|
|
fobos_dev_t* dev = m_dev.load();
|
|
if (!dev || !m_readSync) {
|
|
logLine("reader_loop=ABORT no_device_or_readSync");
|
|
m_readerActive.store(false);
|
|
m_readerFinished.store(true);
|
|
return;
|
|
}
|
|
|
|
std::vector<float> iq(kSyncFloatStorage);
|
|
if (m_convertBuffer.size() < kFifoChunkComplexLength) {
|
|
m_convertBuffer.resize(kFifoChunkComplexLength);
|
|
}
|
|
|
|
bool firstLogged = false;
|
|
unsigned int startupReadErrors = 0;
|
|
uint64_t totalChunks = 0;
|
|
uint64_t shortWrites = 0;
|
|
uint64_t timingWindowReads = 0;
|
|
uint64_t timingWindowChunks = 0;
|
|
uint64_t timingWindowSamples = 0;
|
|
uint64_t timingWindowWritten = 0;
|
|
uint64_t timingWindowShortWrites = 0;
|
|
double timingWindowReadMs = 0.0;
|
|
double timingWindowConvertPushMs = 0.0;
|
|
const auto streamStart = std::chrono::steady_clock::now();
|
|
|
|
logLine("reader_loop=ENTER fast_convert_single_fifo_write=%u", kFifoChunkComplexLength);
|
|
|
|
while (!m_stopRequested.load()) {
|
|
uint32_t actual = 0;
|
|
const auto readStart = std::chrono::steady_clock::now();
|
|
int rr = m_readSync(dev, iq.data(), &actual);
|
|
const auto readEnd = std::chrono::steady_clock::now();
|
|
const double readMs = std::chrono::duration<double, std::milli>(readEnd - readStart).count();
|
|
|
|
if (rr != 0) {
|
|
m_failedReads++;
|
|
logLine("read_sync_error result=%d error='%s' after_reads=%llu", rr, errorName(rr), static_cast<unsigned long long>(m_totalReads));
|
|
if (m_stopRequested.load()) {
|
|
break;
|
|
}
|
|
if (!firstLogged) {
|
|
++startupReadErrors;
|
|
if (startupReadErrors >= kStartupReaderErrorAbort) {
|
|
logLine("startup_read_guard=ABORT no_successful_read_after_errors=%u action=reader_exit_waiting_for_stop", startupReadErrors);
|
|
m_stopRequested.store(true);
|
|
m_running.store(false);
|
|
break;
|
|
}
|
|
Sleep(20);
|
|
} else {
|
|
Sleep(2);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (actual == 0) {
|
|
continue;
|
|
}
|
|
startupReadErrors = 0;
|
|
|
|
double mean = 0.0;
|
|
double power = 0.0;
|
|
double peak = 0.0;
|
|
const int inputMode = m_runtimeInputMode.load();
|
|
const double gainScale = m_iqGain.load() * SDR_RX_SCALEF;
|
|
const double fixLimit = SDR_RX_SCALEF - 1.0;
|
|
const auto convertStart = std::chrono::steady_clock::now();
|
|
|
|
// Fast conversion path:
|
|
// Load the software gain once per read block, convert inline, and write the whole
|
|
// block to the FIFO once. This keeps the source realtime-safe at high sample rates.
|
|
for (uint32_t i = 0; i < actual; ++i) {
|
|
float re = iq[2u*i];
|
|
float im = iq[2u*i + 1u];
|
|
|
|
// Direct-sampling mode handling:
|
|
// mode 0 RF: normal complex IQ; mode 1 IQ direct: normal complex IQ;
|
|
// mode 2 HF1: use I channel as real-only with alternating sign;
|
|
// mode 3 HF2: use Q channel as real-only with alternating sign.
|
|
if (inputMode == 2) {
|
|
if ((i & 1u) != 0u) { re = -re; }
|
|
im = 0.0f;
|
|
} else if (inputMode == 3) {
|
|
re = im;
|
|
if ((i & 1u) != 0u) { re = -re; }
|
|
im = 0.0f;
|
|
}
|
|
|
|
if (!firstLogged) {
|
|
const double reD = static_cast<double>(re);
|
|
const double imD = static_cast<double>(im);
|
|
mean += re + im;
|
|
power += reD*reD + imD*imD;
|
|
peak = std::max(peak, std::max(std::fabs(reD), std::fabs(imD)));
|
|
}
|
|
|
|
double sr = static_cast<double>(re) * gainScale;
|
|
double si = static_cast<double>(im) * gainScale;
|
|
if (sr > fixLimit) { sr = fixLimit; } else if (sr < -fixLimit) { sr = -fixLimit; }
|
|
if (si > fixLimit) { si = fixLimit; } else if (si < -fixLimit) { si = -fixLimit; }
|
|
|
|
// Fast round-to-nearest for normal finite Fobos floats. Fobos Agile API returns finite
|
|
// normalized IQ samples; pathological NaN/Inf protection is intentionally not in this
|
|
// hot path because it was part of the realtime bottleneck.
|
|
m_convertBuffer[i].setReal(static_cast<FixReal>(sr >= 0.0 ? sr + 0.5 : sr - 0.5));
|
|
m_convertBuffer[i].setImag(static_cast<FixReal>(si >= 0.0 ? si + 0.5 : si - 0.5));
|
|
}
|
|
|
|
const auto convertEnd = std::chrono::steady_clock::now();
|
|
const unsigned int writtenThisRead = m_sampleFifo->write(m_convertBuffer.cbegin(), m_convertBuffer.cbegin() + actual);
|
|
const auto pushEnd = std::chrono::steady_clock::now();
|
|
const unsigned int chunksThisRead = 1;
|
|
totalChunks++;
|
|
if (writtenThisRead != actual) {
|
|
shortWrites++;
|
|
timingWindowShortWrites++;
|
|
logLine("fifo_write_short requested=%u written=%u fifo_fill=%u", actual, writtenThisRead, m_sampleFifo->fill());
|
|
}
|
|
|
|
const double convertMs = std::chrono::duration<double, std::milli>(convertEnd - convertStart).count();
|
|
const double pushMs = std::chrono::duration<double, std::milli>(pushEnd - convertEnd).count();
|
|
const double convertPushMs = convertMs + pushMs;
|
|
|
|
m_totalReads++;
|
|
m_totalSamples += actual;
|
|
m_totalWritten += writtenThisRead;
|
|
|
|
timingWindowReads++;
|
|
timingWindowChunks += chunksThisRead;
|
|
timingWindowSamples += actual;
|
|
timingWindowWritten += writtenThisRead;
|
|
timingWindowReadMs += readMs;
|
|
timingWindowConvertPushMs += convertPushMs;
|
|
|
|
if (!firstLogged) {
|
|
const double denom = static_cast<double>(actual) * 2.0;
|
|
logLine("first_read_ok actual=%u written=%u chunks=%u mean=%.9f rms=%.9f peak=%.9f iq_gain=%.3f input_mode=%d read_ms=%.3f convert_ms=%.3f fifo_write_ms=%.3f convert_push_ms=%.3f",
|
|
actual,
|
|
writtenThisRead,
|
|
chunksThisRead,
|
|
mean / denom,
|
|
std::sqrt(power / denom),
|
|
peak,
|
|
m_iqGain.load(),
|
|
m_runtimeInputMode.load(),
|
|
readMs,
|
|
convertMs,
|
|
pushMs,
|
|
convertPushMs);
|
|
firstLogged = true;
|
|
}
|
|
|
|
if ((m_totalReads % 50u) == 0u) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const double elapsedS = std::chrono::duration<double>(now - streamStart).count();
|
|
const double effectiveMsps = elapsedS > 0.0 ? (static_cast<double>(m_totalSamples) / elapsedS / 1.0e6) : 0.0;
|
|
const double avgReadMs = timingWindowReads > 0 ? timingWindowReadMs / static_cast<double>(timingWindowReads) : 0.0;
|
|
const double avgConvertPushMs = timingWindowReads > 0 ? timingWindowConvertPushMs / static_cast<double>(timingWindowReads) : 0.0;
|
|
logLine("stream_timing reads=%llu samples=%llu written=%llu fifo_writes=%llu fifo_fill=%u avg_read_ms=%.3f avg_convert_write_ms=%.3f effective_msps=%.3f window_short_writes=%llu total_short_writes=%llu",
|
|
static_cast<unsigned long long>(m_totalReads),
|
|
static_cast<unsigned long long>(m_totalSamples),
|
|
static_cast<unsigned long long>(m_totalWritten),
|
|
static_cast<unsigned long long>(totalChunks),
|
|
m_sampleFifo->fill(),
|
|
avgReadMs,
|
|
avgConvertPushMs,
|
|
effectiveMsps,
|
|
static_cast<unsigned long long>(timingWindowShortWrites),
|
|
static_cast<unsigned long long>(shortWrites));
|
|
timingWindowReads = 0;
|
|
timingWindowChunks = 0;
|
|
timingWindowSamples = 0;
|
|
timingWindowWritten = 0;
|
|
timingWindowShortWrites = 0;
|
|
timingWindowReadMs = 0.0;
|
|
timingWindowConvertPushMs = 0.0;
|
|
}
|
|
}
|
|
|
|
logLine("stream_loop_exit reads=%llu samples=%llu written=%llu failed_reads=%llu fifo_fill=%u",
|
|
static_cast<unsigned long long>(m_totalReads),
|
|
static_cast<unsigned long long>(m_totalSamples),
|
|
static_cast<unsigned long long>(m_totalWritten),
|
|
static_cast<unsigned long long>(m_failedReads),
|
|
m_sampleFifo->fill());
|
|
|
|
m_readerActive.store(false);
|
|
m_readerFinished.store(true);
|
|
#endif
|
|
}
|
|
|
|
void FOBOSWorker::cleanupAfterReaderJoined(bool readerJoined)
|
|
{
|
|
#ifdef _WIN32
|
|
fobos_dev_t* dev = m_dev.load();
|
|
|
|
if (!dev) {
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
logLine("cleanup=no_device");
|
|
return;
|
|
}
|
|
|
|
if (readerJoined && m_closeDev) {
|
|
Sleep(100);
|
|
logLine("close_call=START_AFTER_READER_JOIN");
|
|
int r = m_closeDev(dev);
|
|
logLine("close_call=RETURN result=%d error='%s'", r, errorName(r));
|
|
} else {
|
|
logLine("close_call=SKIPPED_READER_NOT_JOINED");
|
|
}
|
|
|
|
m_dev.store(nullptr);
|
|
m_running.store(false);
|
|
m_stopRequested.store(false);
|
|
|
|
{
|
|
QMutexLocker sessionLock(&g_sessionMutex);
|
|
g_deviceBusy = false;
|
|
}
|
|
|
|
logLine("freelibrary_call=SKIPPED_PROCESS_SCOPED_DLL");
|
|
logLine("source_result=STOPPED_CLEANLY_READER_JOINED_CLOSE_DONE");
|
|
#endif
|
|
}
|
|
|
|
void FOBOSWorker::logLine(const char* fmt, ...) const
|
|
{
|
|
#if defined(FOBOS_DEBUG_FILE_LOG)
|
|
FILE* f = nullptr;
|
|
fopen_s(&f, kLogPath, "a");
|
|
if (!f) {
|
|
return;
|
|
}
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vfprintf(f, fmt, ap);
|
|
va_end(ap);
|
|
fputc('\n', f);
|
|
fclose(f);
|
|
#else
|
|
(void) fmt;
|
|
#endif
|
|
}
|
|
|
|
void FOBOSWorker::logTimestamp() const
|
|
{
|
|
#if defined(FOBOS_DEBUG_FILE_LOG)
|
|
const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz");
|
|
logLine("timestamp=%s", ts.toUtf8().constData());
|
|
#endif
|
|
}
|
|
|
|
const char* FOBOSWorker::errorName(int code) const
|
|
{
|
|
#ifdef _WIN32
|
|
if (m_errorName) {
|
|
const char* s = m_errorName(code);
|
|
return s ? s : "null_error_name";
|
|
}
|
|
#endif
|
|
return "error_name_unavailable";
|
|
}
|
|
|
|
FixReal FOBOSWorker::floatToFix(float v) const
|
|
{
|
|
// Legacy helper kept for ABI/source compatibility. The realtime reader loop uses an
|
|
// inline fast conversion path and does not call this function per sample.
|
|
if (!std::isfinite(v)) {
|
|
return 0;
|
|
}
|
|
|
|
const double limit = SDR_RX_SCALEF - 1.0;
|
|
double scaled = static_cast<double>(v) * m_iqGain.load() * SDR_RX_SCALEF;
|
|
|
|
if (scaled > limit) {
|
|
scaled = limit;
|
|
} else if (scaled < -limit) {
|
|
scaled = -limit;
|
|
}
|
|
|
|
return static_cast<FixReal>(scaled >= 0.0 ? scaled + 0.5 : scaled - 0.5);
|
|
}
|