Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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})

Expand Down Expand Up @@ -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}" "$<TARGET_FILE_DIR:InmarScope>"
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}" "$<TARGET_FILE_DIR:InmarScope>"
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}" "$<TARGET_FILE_DIR:InmarScope>/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
Expand Down
26 changes: 25 additions & 1 deletion COMPILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
21 changes: 20 additions & 1 deletion src/core/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -73,9 +76,15 @@ struct App
std::vector<SdrDeviceInfo> 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";
Expand All @@ -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;
Expand Down
94 changes: 83 additions & 11 deletions src/gui/gui_panels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading