diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c05dbc..9045793 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,25 @@ if(AIRSPY_LIBRARIES) message(STATUS "Airspy link library found: ${AIRSPY_LIBRARIES}") endif() +# libiio (native ADALM-Pluto / Pluto+ source, optional). +if(PkgConfig_FOUND) + pkg_check_modules(LIBIIO QUIET libiio) +endif() +if(NOT LIBIIO_FOUND) + find_path(LIBIIO_INCLUDE_DIRS NAMES iio.h + HINTS $ENV{LIBIIO_ROOT} + PATH_SUFFIXES include) + find_library(LIBIIO_LIBRARIES NAMES iio libiio + HINTS $ENV{LIBIIO_ROOT} + PATH_SUFFIXES lib lib64 Windows-MinGW-W64) + if(LIBIIO_INCLUDE_DIRS AND LIBIIO_LIBRARIES) + set(LIBIIO_FOUND TRUE) + endif() +endif() +if(LIBIIO_FOUND) + message(STATUS "libiio found: enabling Pluto+/AD936x source") +endif() + set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/third_party/imgui) option(ENABLE_SDRPLAY "Enable SDRplay via the SoapySDR C API" ON) option(REQUIRE_SDRPLAY "Fail configuration if the SDRplay backend cannot be built" OFF) @@ -239,6 +258,11 @@ if(AIRSPY_FOUND) target_compile_definitions(InmarScope PRIVATE HAS_AIRSPY=1) target_include_directories(InmarScope PRIVATE ${AIRSPY_INCLUDE_DIRS}) endif() +if(LIBIIO_FOUND) + target_sources(InmarScope PRIVATE src/sdr/pluto_source.cpp) + target_compile_definitions(InmarScope PRIVATE HAS_LIBIIO=1) + target_include_directories(InmarScope PRIVATE ${LIBIIO_INCLUDE_DIRS}) +endif() find_library(ZSTD_LIBRARIES NAMES zstd libzstd REQUIRED) find_library(ZLIB_LIB NAMES z libz zlib REQUIRED) find_library(SQLITE3_LIB NAMES sqlite3 libsqlite3 REQUIRED) @@ -268,6 +292,10 @@ target_link_libraries(InmarScope PRIVATE Threads::Threads) if(AIRSPY_FOUND) target_link_libraries(InmarScope PRIVATE ${AIRSPY_LIBRARIES}) endif() +if(LIBIIO_FOUND) + target_link_libraries(InmarScope PRIVATE ${LIBIIO_LIBRARIES}) + target_link_directories(InmarScope PRIVATE ${LIBIIO_LIBRARY_DIRS}) +endif() target_link_directories(InmarScope PRIVATE ${RTLSDR_LIBRARY_DIRS} ${OGG_LIBRARY_DIRS} ${VORBIS_LIBRARY_DIRS} ${VORBISENC_LIBRARY_DIRS}) @@ -322,6 +350,39 @@ if(WIN32) message(WARNING "Runtime DLL not found: ${MINGW_BIN}/${dll}") endif() endforeach() + if(LIBIIO_FOUND) + find_file(LIBIIO_RUNTIME_DLL NAMES libiio.dll iio.dll + HINTS $ENV{LIBIIO_ROOT} ${LIBIIO_LIBRARY_DIRS} ${MINGW_BIN} + PATH_SUFFIXES bin lib Windows-MinGW-W64) + if(LIBIIO_RUNTIME_DLL) + add_custom_command(TARGET InmarScope POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${LIBIIO_RUNTIME_DLL}" "$" + COMMENT "Copying libiio runtime") + else() + message(WARNING "libiio runtime DLL not found; place it next to InmarScope.exe") + endif() + # The official libiio MinGW archive also needs libserialport and names + # its bundled libxml2 dependency differently from the DLL import name. + find_file(LIBIIO_SERIALPORT_DLL NAMES libserialport-0.dll + HINTS $ENV{LIBIIO_ROOT} + PATH_SUFFIXES bin Windows-MinGW-W64) + if(LIBIIO_SERIALPORT_DLL) + add_custom_command(TARGET InmarScope POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${LIBIIO_SERIALPORT_DLL}" "$" + COMMENT "Copying libiio serial backend") + endif() + find_file(LIBIIO_XML2_DLL NAMES libxml2.dll libxml2-2.dll + HINTS $ENV{LIBIIO_ROOT} + PATH_SUFFIXES bin Windows-MinGW-W64) + if(LIBIIO_XML2_DLL) + add_custom_command(TARGET InmarScope POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${LIBIIO_XML2_DLL}" "$/libxml2.dll" + COMMENT "Copying libiio XML backend") + endif() + endif() # WebView2Loader.dll is not in the MINGW bin dir — copy from vendored SDK. add_custom_command(TARGET InmarScope POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/COMPILE.md b/COMPILE.md index d4f471d..5637711 100644 --- a/COMPILE.md +++ b/COMPILE.md @@ -88,6 +88,23 @@ View more information at the [airspy/airspyone_host](https://github.com/airspy/a Airspy headers are vendored in `third_party/airspy/`. The build automatically enables Airspy (`HAS_AIRSPY=1`) when libairspy is found by CMake. +#### Optional: ADALM-Pluto / Pluto+ support + +Download and extract the official `Windows.zip` from the Analog Devices +[`libiio` release](https://github.com/analogdevicesinc/libiio/releases/latest), +then point `LIBIIO_ROOT` at the extracted directory. It must contain `include/` +and `Windows-MinGW-W64/`: + +```bash +export LIBIIO_ROOT='C:/path/to/libiio' +cmake -S . -B build -G Ninja +``` + +The build enables the native Pluto+/AD936x source (`HAS_LIBIIO=1`) when both +`iio.h` and the MinGW libiio import library are found. It copies `libiio.dll` +and the two additional runtime DLLs required by the official archive next to +the executable. + ### 3. Clone the InmarScope repository Obtain the project source code using Git in the **MINGW64** shell: @@ -118,7 +135,8 @@ build/libgcc_s_seh-1.dll, libwinpthread-1.dll, libstdc++-6.dll, libjansson-4.dll, WebView2Loader.dll ``` -(plus `build/libairspy.dll` when Airspy support is enabled) +(plus `build/libairspy.dll` and/or `build/libiio.dll` when those optional +sources are enabled) The DLLs are copied next to the `.exe` automatically (POST_BUILD step), so it runs standalone from a double-click or from PyCharm without MSYS2 on `PATH`. @@ -217,6 +235,12 @@ Install `libairspy-dev` (Debian/Ubuntu), `airspy` (Arch), or `airspy-devel` (Fedora). The build enables Airspy (`HAS_AIRSPY=1`) automatically when libairspy is found by CMake. +#### Optional: ADALM-Pluto / Pluto+ support + +Install `libiio-dev` (Debian/Ubuntu), `libiio` (Arch), or `libiio-devel` +(Fedora). The build enables the native Pluto+/AD936x source (`HAS_LIBIIO=1`) +automatically when libiio is found by CMake. + ### 2. Clone the InmarScope repository Obtain the project source code using Git: diff --git a/README.md b/README.md index 8bf9d04..8826bd0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Multi-protocol L-band satellite communications decoder for Windows (and Linux if you compile it). -Decodes Inmarsat Aero (Classic Aero, Aero-H/H+) and Inmarsat-C/EGC signals in real time using RTL-SDR, HackRF, SDRplay, or SDR++ server sources. Voice call recording, aircraft tracking, and message output in one application. +Decodes Inmarsat Aero (Classic Aero, Aero-H/H+) and Inmarsat-C/EGC signals in real time using RTL-SDR, HackRF, Airspy, Pluto+/AD936x, RTL-TCP, SDRplay, or SDR++ server sources. Voice call recording, aircraft tracking, and message output in one application. [![Animation showing InmarScope decoding messages and displaying waterfall graphics](screenshot.gif)](https://sarahsforge.dev/products/inmarscope) diff --git a/src/core/app.h b/src/core/app.h index 6ca3b02..2b2243e 100644 --- a/src/core/app.h +++ b/src/core/app.h @@ -9,6 +9,9 @@ #ifdef HAS_AIRSPY #include "sdr/airspy_source.h" #endif +#ifdef HAS_LIBIIO +#include "sdr/pluto_source.h" +#endif #include "sdr/wav_file_source.h" #include "sdr/sdrpp_server_source.h" #include "sdr/rtl_tcp_source.h" @@ -73,9 +76,15 @@ struct App std::vector rspDevices; #ifdef HAS_AIRSPY AirspySource airspy; +#endif +#ifdef HAS_LIBIIO + PlutoSource pluto; #endif SdrSource* active = &sdr; - int sourceMode = 0; // 0=RTL, 1=WAV, 2=SDR++ Server, 3=HackRF, 4=Dual RTL, 5=Airspy, 6=RTL-TCP, 7=SDRplay + // Values are stable because they are persisted in inmarscope.ini. + // 0=RTL, 1=WAV, 2=SDR++ Server, 3=HackRF, 4=Dual RTL, + // 5=Airspy, 6=RTL-TCP, 7=SDRplay, 8=Pluto+/AD936x. + int sourceMode = 0; char wavPath[512] = ""; bool wavLoop = true; char serverHost[128] = "localhost"; @@ -86,6 +95,16 @@ struct App char rtlTcpHost[128] = "127.0.0.1"; int rtlTcpPort = 1234; + // ADALM-Pluto / Pluto+ (libiio) +#ifdef HAS_LIBIIO + char plutoUri[128] = "ip:192.168.2.1"; + double plutoSampleRateMHz = 2.4; + double plutoBandwidthMHz = 2.0; + bool plutoAgc = false; + float plutoGainDb = 40.0f; + int plutoRfPort = 0; // 0=A_BALANCED, 1=B_BALANCED +#endif + // HackRF double hackSampleRateMHz = 10.0; int hackLna = 16; diff --git a/src/gui/gui_panels.cpp b/src/gui/gui_panels.cpp index b5ca689..cb5cde6 100644 --- a/src/gui/gui_panels.cpp +++ b/src/gui/gui_panels.cpp @@ -159,17 +159,40 @@ void drawControls(App& app) bool running = app.active->running(); ImGui::BeginDisabled(running); - const char* modes[] = {"RTL-SDR", "WAV file", "SDR++ Server", "HackRF", "Dual RTL", "Airspy", "RTL-TCP", "SDRplay"}; - if (app.sourceMode < 0 || app.sourceMode > 7) app.sourceMode = 0; - if (ImGui::BeginCombo(_L("Source"), modes[app.sourceMode])) { - for (int mode = 0; mode < 8; ++mode) { -#ifndef HAS_AIRSPY - if (mode == 5) continue; + struct SourceChoice { const char* label; int mode; }; + const SourceChoice modes[] = { + {"RTL-SDR", 0}, {"WAV file", 1}, {"SDR++ Server", 2}, + {"HackRF", 3}, {"Dual RTL", 4}, +#ifdef HAS_AIRSPY + {"Airspy", 5}, +#endif + {"RTL-TCP", 6}, {"SDRplay", 7}, +#ifdef HAS_LIBIIO + {"Pluto+ / AD936x", 8} #endif - if (ImGui::Selectable(modes[mode], app.sourceMode == mode)) { + }; + const char* sourcePreview = "Unavailable source"; + bool sourceAvailable = false; + for (const auto& mode : modes) + { + if (mode.mode == app.sourceMode) + { + sourcePreview = mode.label; + sourceAvailable = true; + } + } + if (ImGui::BeginCombo(_L("Source"), sourcePreview)) + { + for (const auto& mode : modes) + { + bool selected = mode.mode == app.sourceMode; + if (ImGui::Selectable(mode.label, selected)) { app.rspB.close(); app.rsp.close(); - app.sourceMode = mode; app.devices.clear(); app.deviceIndex = 0; + app.sourceMode = mode.mode; + app.devices.clear(); app.deviceIndex = 0; } + if (selected) + ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } @@ -179,7 +202,8 @@ void drawControls(App& app) if (!running) { - bool canStart = (app.sourceMode == 1) ? (app.wavPath[0] != '\0') : true; + bool canStart = sourceAvailable && + ((app.sourceMode == 1) ? (app.wavPath[0] != '\0') : true); ImGui::BeginDisabled(!canStart); if (ImGui::Button(_L("Start"), ImVec2(120, 0))) startActive(app); @@ -565,10 +589,58 @@ void drawControls(App& app) if (running) app.airspy.setDcBlock(app.dcBlock); } } +#endif +#ifdef HAS_LIBIIO + else if (app.sourceMode == 8) + { + // ---- ADALM-Pluto / Pluto+ (native libiio) ---- + ImGui::BeginDisabled(running); + ImGui::InputText("IIO URI", app.plutoUri, sizeof(app.plutoUri)); + ImGui::EndDisabled(); + ImGui::TextDisabled("Examples: ip:192.168.2.1 or usb:1.2.3"); + + if (ImGui::InputDouble("Center (MHz)", &app.centerFreqMHz, 0.1, 1.0, "%.4f")) + { + app.viewA.resetView = true; + if (running) + app.pluto.setCenterFreq(app.centerFreqMHz * 1e6); + } + if (ImGui::InputDouble("Sample rate (MHz)", &app.plutoSampleRateMHz, + 0.1, 1.0, "%.3f")) + { + app.plutoSampleRateMHz = std::clamp(app.plutoSampleRateMHz, 0.521, 61.44); + app.viewA.resetView = true; + if (running) + app.pluto.setSampleRate(app.plutoSampleRateMHz * 1e6); + } + if (ImGui::InputDouble("RF bandwidth (MHz)", &app.plutoBandwidthMHz, + 0.1, 1.0, "%.3f")) + { + app.plutoBandwidthMHz = std::clamp(app.plutoBandwidthMHz, 0.2, 56.0); + if (running) + app.pluto.setBandwidth(app.plutoBandwidthMHz * 1e6); + } + + const char* rfPorts[] = {"RX1 (A_BALANCED)", "RX2 (B_BALANCED)"}; + if (ImGui::Combo("RF input", &app.plutoRfPort, rfPorts, 2) && running) + app.pluto.setRfPort(app.plutoRfPort == 1 ? "B_BALANCED" : "A_BALANCED"); + + if (ImGui::Checkbox("Slow-attack AGC", &app.plutoAgc) && running) + app.pluto.setGain(app.plutoAgc ? -1.0 : (double)app.plutoGainDb); + if (!app.plutoAgc) + { + if (ImGui::SliderFloat("RX gain (dB)", &app.plutoGainDb, -3.0f, 73.0f, "%.1f") && running) + app.pluto.setGain((double)app.plutoGainDb); + } + if (drawPpmAdjust("PPM", &app.ppm) && running) + app.pluto.setPpm((double)app.ppm); + if (ImGui::Checkbox(_L("DC block"), &app.dcBlock) && running) + app.pluto.setDcBlock(app.dcBlock); + } #endif if (app.sourceMode == 6) -{ -// ---- RTL-TCP (network) ---- + { + // ---- RTL-TCP (network) ---- ImGui::SetNextItemWidth(-60.0f); ImGui::InputText("Host", app.rtlTcpHost, sizeof(app.rtlTcpHost)); ImGui::InputInt("Port", &app.rtlTcpPort); diff --git a/src/sdr/pluto_source.cpp b/src/sdr/pluto_source.cpp new file mode 100644 index 0000000..5b708d7 --- /dev/null +++ b/src/sdr/pluto_source.cpp @@ -0,0 +1,335 @@ +#include "sdr/pluto_source.h" + +#include "util/log.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { +std::string iioError(int code) +{ + char buf[256] = {}; + iio_strerror(code < 0 ? -code : code, buf, sizeof(buf)); + return buf[0] ? std::string(buf) : std::string("unknown IIO error"); +} + +bool writeLongLong(iio_channel* ch, const char* attr, long long value, + std::string& err) +{ + int rc = iio_channel_attr_write_longlong(ch, attr, value); + if (rc >= 0) + return true; + err = std::string("Could not set ") + attr + ": " + iioError(rc); + return false; +} + +int32_t decodeSample(const char* ptr, const iio_data_format* fmt) +{ + uint16_t raw = 0; + std::memcpy(&raw, ptr, sizeof(raw)); + if (fmt->is_be) + raw = (uint16_t)((raw >> 8) | (raw << 8)); + + uint32_t value = (uint32_t)raw >> fmt->shift; + unsigned bits = std::min(fmt->bits, 16u); + if (bits == 0) + return 0; + uint32_t mask = bits == 16 ? 0xffffu : ((1u << bits) - 1u); + value &= mask; + if (fmt->is_signed && (value & (1u << (bits - 1)))) + value |= ~mask; + return (int32_t)value; +} +} // namespace + +PlutoSource::~PlutoSource() +{ + stop(); +} + +std::vector PlutoSource::listDevices() +{ + std::vector out; + iio_context* ctx = iio_create_context_from_uri(uri_.c_str()); + if (!ctx) + return out; + + iio_device* phy = iio_context_find_device(ctx, "ad9361-phy"); + if (!phy) + phy = iio_context_find_device(ctx, "ad9364-phy"); + if (phy) + { + SdrDeviceInfo info; + info.index = 0; + const char* name = iio_device_get_name(phy); + info.name = name ? name : "AD936x"; + info.serial = uri_; + out.push_back(std::move(info)); + } + iio_context_destroy(ctx); + return out; +} + +bool PlutoSource::openContext(std::string& err) +{ + ctx_ = iio_create_context_from_uri(uri_.c_str()); + if (!ctx_) + { + err = "Could not open IIO context '" + uri_ + "': " + iioError(errno); + return false; + } + + phy_ = iio_context_find_device(ctx_, "ad9361-phy"); + if (!phy_) + phy_ = iio_context_find_device(ctx_, "ad9364-phy"); + rxDev_ = iio_context_find_device(ctx_, "cf-ad9361-lpc"); + if (!rxDev_) + rxDev_ = iio_context_find_device(ctx_, "cf-ad9361-A"); + if (!phy_ || !rxDev_) + { + err = "The IIO context does not expose an AD936x PHY and RX streaming device"; + return false; + } + + rxCfg_ = iio_device_find_channel(phy_, "voltage0", false); + rxLo_ = iio_device_find_channel(phy_, "altvoltage0", true); + rxI_ = iio_device_find_channel(rxDev_, "voltage0", false); + if (!rxI_) + rxI_ = iio_device_find_channel(rxDev_, "altvoltage0", false); + rxQ_ = iio_device_find_channel(rxDev_, "voltage1", false); + if (!rxQ_) + rxQ_ = iio_device_find_channel(rxDev_, "altvoltage1", false); + if (!rxCfg_ || !rxLo_ || !rxI_ || !rxQ_) + { + err = "The AD936x IIO channels required for RX were not found"; + return false; + } + return true; +} + +bool PlutoSource::configure(std::string& err) +{ + int rc = iio_channel_attr_write(rxCfg_, "rf_port_select", rfPort_.c_str()); + if (rc < 0) + { + err = "Could not select Pluto RX port: " + iioError(rc); + return false; + } + if (!writeLongLong(rxCfg_, "rf_bandwidth", (long long)std::llround(bandwidth_), err) || + !writeLongLong(rxCfg_, "sampling_frequency", (long long)std::llround(sampleRate_), err) || + !writeLongLong(rxLo_, "frequency", + (long long)std::llround(centerFreq_ * (1.0 + ppm_ / 1e6)), err)) + return false; + + rc = iio_channel_attr_write(rxCfg_, "gain_control_mode", + agc_ ? "slow_attack" : "manual"); + if (rc < 0) + { + err = "Could not set Pluto gain-control mode: " + iioError(rc); + return false; + } + if (!agc_) + { + rc = iio_channel_attr_write_double(rxCfg_, "hardwaregain", gainDb_); + if (rc < 0) + { + err = "Could not set Pluto RX gain: " + iioError(rc); + return false; + } + } + iio_channel_enable(rxI_); + iio_channel_enable(rxQ_); + + const iio_data_format* fmtI = iio_channel_get_data_format(rxI_); + const iio_data_format* fmtQ = iio_channel_get_data_format(rxQ_); + if (!fmtI || !fmtQ || fmtI->length != 16 || fmtQ->length != 16 || + fmtI->bits == 0 || fmtI->bits > 16 || fmtQ->bits == 0 || fmtQ->bits > 16) + { + err = "Unsupported Pluto RX sample format (expected 16-bit I/Q containers)"; + return false; + } + + rxBuf_ = iio_device_create_buffer(rxDev_, 32768, false); + if (!rxBuf_) + { + err = "Could not create Pluto RX buffer: " + iioError(errno); + return false; + } + return true; +} + +bool PlutoSource::start(int, SdrSampleCb cb, std::string& err) +{ + if (running_.load()) + return true; + if (readThread_.joinable()) + readThread_.join(); + cleanup(); + if (!openContext(err) || !configure(err)) + { + cleanup(); + return false; + } + + cb_ = std::move(cb); + dcOffRe_ = dcOffIm_ = 0.0f; + dcRate_ = (float)(50.0 / sampleRate_); + running_.store(true); + readThread_ = std::thread(&PlutoSource::readLoop, this); + return true; +} + +void PlutoSource::stop() +{ + running_.store(false); + if (rxBuf_) + iio_buffer_cancel(rxBuf_); + if (readThread_.joinable()) + readThread_.join(); + cleanup(); +} + +void PlutoSource::cleanup() +{ + if (rxBuf_) + { + iio_buffer_destroy(rxBuf_); + rxBuf_ = nullptr; + } + if (rxI_) + iio_channel_disable(rxI_); + if (rxQ_) + iio_channel_disable(rxQ_); + rxI_ = rxQ_ = rxCfg_ = rxLo_ = nullptr; + phy_ = rxDev_ = nullptr; + if (ctx_) + { + iio_context_destroy(ctx_); + ctx_ = nullptr; + } + cb_ = nullptr; +} + +void PlutoSource::readLoop() +{ + const iio_data_format* fmtI = iio_channel_get_data_format(rxI_); + const iio_data_format* fmtQ = iio_channel_get_data_format(rxQ_); + const float normI = (float)(1u << (std::min(fmtI->bits, 16u) - 1)); + const float normQ = (float)(1u << (std::min(fmtQ->bits, 16u) - 1)); + + while (running_.load()) + { + ssize_t nbytes = iio_buffer_refill(rxBuf_); + if (nbytes < 0) + { + if (running_.load()) + logWrite("Pluto: RX refill failed: %s", iioError((int)nbytes).c_str()); + break; + } + + ptrdiff_t step = iio_buffer_step(rxBuf_); + ssize_t sampleBytes = iio_device_get_sample_size(rxDev_); + if (step <= 0 || sampleBytes <= 0) + break; + size_t count = (size_t)nbytes / (size_t)sampleBytes; + scratch_.resize(count * 2); + const char* pI = static_cast(iio_buffer_first(rxBuf_, rxI_)); + const char* pQ = static_cast(iio_buffer_first(rxBuf_, rxQ_)); + + float offRe = dcOffRe_; + float offIm = dcOffIm_; + const float rate = dcRate_; + const bool blockDc = dcBlock_.load(); + for (size_t i = 0; i < count; ++i, pI += step, pQ += step) + { + float re = (float)decodeSample(pI, fmtI) / normI; + float im = (float)decodeSample(pQ, fmtQ) / normQ; + if (blockDc) + { + float outRe = re - offRe; + offRe += outRe * rate; + float outIm = im - offIm; + offIm += outIm * rate; + re = outRe; + im = outIm; + } + scratch_[i * 2] = re; + scratch_[i * 2 + 1] = im; + } + dcOffRe_ = offRe; + dcOffIm_ = offIm; + if (cb_ && count > 0) + cb_(scratch_.data(), (int)count); + } + running_.store(false); +} + +void PlutoSource::applyTune() +{ + if (rxLo_) + iio_channel_attr_write_longlong( + rxLo_, "frequency", + (long long)std::llround(centerFreq_ * (1.0 + ppm_ / 1e6))); +} + +void PlutoSource::applyGain() +{ + if (!rxCfg_) + return; + if (agc_) + iio_channel_attr_write(rxCfg_, "gain_control_mode", "slow_attack"); + else + { + iio_channel_attr_write(rxCfg_, "gain_control_mode", "manual"); + iio_channel_attr_write_double(rxCfg_, "hardwaregain", gainDb_); + } +} + +void PlutoSource::setCenterFreq(double hz) +{ + centerFreq_ = hz; + applyTune(); +} + +void PlutoSource::setSampleRate(double hz) +{ + sampleRate_ = hz; + if (rxCfg_) + iio_channel_attr_write_longlong(rxCfg_, "sampling_frequency", + (long long)std::llround(hz)); +} + +void PlutoSource::setBandwidth(double hz) +{ + bandwidth_ = hz; + if (rxCfg_) + iio_channel_attr_write_longlong(rxCfg_, "rf_bandwidth", + (long long)std::llround(hz)); +} + +void PlutoSource::setGain(double db) +{ + agc_ = db < 0.0; + if (!agc_) + gainDb_ = std::clamp(db, -3.0, 73.0); + applyGain(); +} + +void PlutoSource::setRfPort(const std::string& port) +{ + rfPort_ = port; + if (rxCfg_) + iio_channel_attr_write(rxCfg_, "rf_port_select", rfPort_.c_str()); +} + +void PlutoSource::setPpm(double ppm) +{ + ppm_ = ppm; + applyTune(); +} diff --git a/src/sdr/pluto_source.h b/src/sdr/pluto_source.h new file mode 100644 index 0000000..e5ba855 --- /dev/null +++ b/src/sdr/pluto_source.h @@ -0,0 +1,78 @@ +// Native ADALM-Pluto / Pluto+ source backend using libiio. +#pragma once + +#include "sdr/sdr_source.h" + +#include +#include +#include +#include + +struct iio_buffer; +struct iio_channel; +struct iio_context; +struct iio_device; + +class PlutoSource : public SdrSource +{ +public: + PlutoSource() = default; + ~PlutoSource() override; + + std::vector listDevices() override; + + void setUri(const std::string& uri) { uri_ = uri; } + const std::string& uri() const { return uri_; } + + void setCenterFreq(double hz) override; + void setSampleRate(double hz) override; + void setGain(double db) override; // <0 selects slow-attack AGC + void setBiasTee(bool) override {} // Pluto hardware has no controllable bias tee + void setPpm(double ppm) override; + + void setBandwidth(double hz); + void setRfPort(const std::string& port); + void setDcBlock(bool on) { dcBlock_.store(on); } + + double centerFreq() const override { return centerFreq_; } + double sampleRate() const override { return sampleRate_; } + + bool start(int deviceIndex, SdrSampleCb cb, std::string& err) override; + void stop() override; + bool running() const override { return running_.load(); } + +private: + bool openContext(std::string& err); + bool configure(std::string& err); + void cleanup(); + void readLoop(); + void applyTune(); + void applyGain(); + + std::string uri_ = "ip:192.168.2.1"; + std::string rfPort_ = "A_BALANCED"; + double centerFreq_ = 1545.0e6; + double sampleRate_ = 2.4e6; + double bandwidth_ = 2.0e6; + double gainDb_ = 40.0; + double ppm_ = 0.0; + bool agc_ = false; + + iio_context* ctx_ = nullptr; + iio_device* phy_ = nullptr; + iio_device* rxDev_ = nullptr; + iio_channel* rxCfg_ = nullptr; + iio_channel* rxLo_ = nullptr; + iio_channel* rxI_ = nullptr; + iio_channel* rxQ_ = nullptr; + iio_buffer* rxBuf_ = nullptr; + + std::atomic running_{false}; + std::atomic dcBlock_{true}; + std::thread readThread_; + SdrSampleCb cb_; + std::vector scratch_; + float dcOffRe_ = 0.0f; + float dcOffIm_ = 0.0f; + float dcRate_ = 2.0e-6f; +}; diff --git a/src/session/session.cpp b/src/session/session.cpp index b5bcbe5..a519fed 100644 --- a/src/session/session.cpp +++ b/src/session/session.cpp @@ -287,6 +287,21 @@ void startActive(App& app) app.rtltcp.setPpm((double)app.ppm); ok = app.rtltcp.start(0, cb, err); } +#ifdef HAS_LIBIIO + else if (app.sourceMode == 7) + { + app.active = &app.pluto; + app.pluto.setUri(app.plutoUri); + app.pluto.setSampleRate(app.plutoSampleRateMHz * 1e6); + app.pluto.setBandwidth(app.plutoBandwidthMHz * 1e6); + app.pluto.setCenterFreq(app.centerFreqMHz * 1e6); + app.pluto.setRfPort(app.plutoRfPort == 1 ? "B_BALANCED" : "A_BALANCED"); + app.pluto.setGain(app.plutoAgc ? -1.0 : (double)app.plutoGainDb); + app.pluto.setPpm((double)app.ppm); + app.pluto.setDcBlock(app.dcBlock); + ok = app.pluto.start(0, cb, err); + } +#endif else if (app.sourceMode == 7) { @@ -434,4 +449,3 @@ void startActive(App& app) else app.status = "Error: " + err; } - diff --git a/src/state/config.cpp b/src/state/config.cpp index c720c51..2bb95bc 100644 --- a/src/state/config.cpp +++ b/src/state/config.cpp @@ -54,6 +54,10 @@ void cfgWriteAll(App& app, ImGuiTextBuffer* buf) WS(serverHost); WI(serverPort); WI(serverCompression); WI(serverSampleType); WD(serverSampleRateMHz); WS(rtlTcpHost); WI(rtlTcpPort); +#ifdef HAS_LIBIIO + WS(plutoUri); WD(plutoSampleRateMHz); WD(plutoBandwidthMHz); + WI(plutoAgc); WF(plutoGainDb); WI(plutoRfPort); +#endif WD(hackSampleRateMHz); WI(hackLna); WI(hackVga); WI(hackAmp); WI(hackBias); #ifdef HAS_AIRSPY WI(airspySampleRateIdx); WI(airspyGainMode); WI(airspySenseGain); WI(airspyLinearGain); @@ -129,6 +133,10 @@ void cfgReadLine(App& app, const char* line) RS(serverHost); RI(serverPort); RB(serverCompression); RI(serverSampleType); RD(serverSampleRateMHz); RS(rtlTcpHost); RI(rtlTcpPort); +#ifdef HAS_LIBIIO + RS(plutoUri); RD(plutoSampleRateMHz); RD(plutoBandwidthMHz); + RB(plutoAgc); RF(plutoGainDb); RI(plutoRfPort); +#endif RD(hackSampleRateMHz); RI(hackLna); RI(hackVga); RB(hackAmp); RB(hackBias); #ifdef HAS_AIRSPY RI(airspySampleRateIdx); RI(airspyGainMode); RI(airspySenseGain); RI(airspyLinearGain); diff --git a/src/voice/voice_ops.cpp b/src/voice/voice_ops.cpp index 0d43b09..1a6cc15 100644 --- a/src/voice/voice_ops.cpp +++ b/src/voice/voice_ops.cpp @@ -589,4 +589,3 @@ void updateCallHunter(App& app) ++j; } } -