/////////////////////////////////////////////////////////////////////////////////// // 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 #include #include #include #include #include #include #include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include #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(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(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(kDefaultFrequencyHz); fobos_dev_t* dev = m_dev.load(); if (dev && m_runtimeInputMode.load() == 0) { int r = callSetFrequency(dev, static_cast(m_centerFrequencyHz)); logLine("live_frequency_call=RETURN result=%d error='%s' requested_hz=%llu", r, errorName(r), static_cast(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(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(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(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(GetProcAddress(lib, "fobos_sdr_get_api_info")); auto getDeviceCount = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_device_count")); auto listDevices = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_list_devices")); auto openDev = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_open")); m_closeDev = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_close")); auto getBoardInfo = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_board_info")); m_errorName = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_error_name")); m_setFrequency = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_frequency")); auto setFrequency = m_setFrequency; m_setSamplerate = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_samplerate")); m_getSamplerates = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_get_samplerates")); m_setDirectSampling = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_direct_sampling")); m_setAutoBandwidth = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_auto_bandwidth")); m_setUserGpo = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_user_gpo")); m_setClkSource = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_clk_source")); auto setSamplerate = m_setSamplerate; m_setLnaGain = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_lna_gain")); m_setVgaGain = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_set_vga_gain")); auto setLnaGain = m_setLnaGain; auto setVgaGain = m_setVgaGain; auto startSync = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_start_sync")); m_readSync = reinterpret_cast(GetProcAddress(lib, "fobos_sdr_read_sync")); m_stopSync = reinterpret_cast(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(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(centerFrequency)); logLine("set_frequency_call=RETURN result=%d error='%s'", r, errorName(r)); r = callSetSamplerate(dev, static_cast(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 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(GetProcAddress(lib, "fobos_rx_get_api_info")); auto getDeviceCount = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_device_count")); auto listDevices = reinterpret_cast(GetProcAddress(lib, "fobos_rx_list_devices")); auto openDev = reinterpret_cast(GetProcAddress(lib, "fobos_rx_open")); m_closeDev = reinterpret_cast(GetProcAddress(lib, "fobos_rx_close")); auto getBoardInfo = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_board_info")); m_errorName = reinterpret_cast(GetProcAddress(lib, "fobos_rx_error_name")); m_regularSetFrequency = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_frequency")); m_regularSetSamplerate = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_samplerate")); m_regularGetSamplerates = reinterpret_cast(GetProcAddress(lib, "fobos_rx_get_samplerates")); m_regularSetDirectSampling = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_direct_sampling")); m_regularSetUserGpo = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_user_gpo")); m_regularSetClkSource = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_clk_source")); m_regularSetLnaGain = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_lna_gain")); m_regularSetVgaGain = reinterpret_cast(GetProcAddress(lib, "fobos_rx_set_vga_gain")); auto startSync = reinterpret_cast(GetProcAddress(lib, "fobos_rx_start_sync")); m_readSync = reinterpret_cast(GetProcAddress(lib, "fobos_rx_read_sync")); m_stopSync = reinterpret_cast(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(centerFrequency)); logLine("regular_set_frequency_call=RETURN result=%d error='%s'", r, errorName(r)); r = callSetSamplerate(dev, static_cast(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 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(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(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(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 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(readEnd - readStart).count(); if (rr != 0) { m_failedReads++; logLine("read_sync_error result=%d error='%s' after_reads=%llu", rr, errorName(rr), static_cast(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(re); const double imD = static_cast(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(re) * gainScale; double si = static_cast(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(sr >= 0.0 ? sr + 0.5 : sr - 0.5)); m_convertBuffer[i].setImag(static_cast(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(convertEnd - convertStart).count(); const double pushMs = std::chrono::duration(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(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(now - streamStart).count(); const double effectiveMsps = elapsedS > 0.0 ? (static_cast(m_totalSamples) / elapsedS / 1.0e6) : 0.0; const double avgReadMs = timingWindowReads > 0 ? timingWindowReadMs / static_cast(timingWindowReads) : 0.0; const double avgConvertPushMs = timingWindowReads > 0 ? timingWindowConvertPushMs / static_cast(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(m_totalReads), static_cast(m_totalSamples), static_cast(m_totalWritten), static_cast(totalChunks), m_sampleFifo->fill(), avgReadMs, avgConvertPushMs, effectiveMsps, static_cast(timingWindowShortWrites), static_cast(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(m_totalReads), static_cast(m_totalSamples), static_cast(m_totalWritten), static_cast(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(v) * m_iqGain.load() * SDR_RX_SCALEF; if (scaled > limit) { scaled = limit; } else if (scaled < -limit) { scaled = -limit; } return static_cast(scaled >= 0.0 ? scaled + 0.5 : scaled - 0.5); }