1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-06-02 14:04:46 -04:00
Files
sdrangel/plugins/samplesource/fobos/fobosworker.cpp
T
2026-05-27 18:58:28 +03:00

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);
}