Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

sdr-- logo

Welcome to sdr–

sdr– receives and decodes radio signals. Connect an SDR, a network receiver, or an IQ recording to channels and displays on a canvas. Use the rack view for the controls you operate regularly.

The Rust server runs the hardware, signal processing, decoders, and recordings. The desktop app and browser use the same interface to control it:

SDR or recording → Rust server → desktop app or browser

Run the server on your computer or on a separate machine near the antenna. All connected clients share the active receiver. A built-in signal generator lets you learn the controls without hardware.

Start here

Project status

sdr– is under active development. The channel catalog lists supported modes and their maturity. Most decoders are tested with generated fixtures; that does not establish how well they handle signals from real transmitters.

Nightly builds follow main and may change saved-data formats without migration guarantees. Use stable releases for persistent installations.

Install sdr–

sdr– is distributed as a desktop application, a portable headless server, and a container. All three run the same receiver engine and serve the same interface.

Desktop application

The desktop app is the simplest option for a radio connected directly to your computer. Download the installer for your platform from GitHub Releases:

PlatformPackages
macOS.dmg for Apple silicon and Intel
Linux.deb and .AppImage
Windows.msi and .exe installers

The app starts its receiver server on a private loopback port and opens the interface in a native window. Desktop installers include SoapySDR and the supported hardware modules. SDRplay receivers also require the separately installed vendor API; see SDRplay receivers. CR-8 receivers need the vendor library.

Portable server

Portable sdrmm archives are useful on a Raspberry Pi, home server, or machine you want to access from another browser. Unpack the archive and run:

./sdrmm

The server listens on every interface at port 8080 by default. Open http://<server>:8080 from a browser on the same network.

Portable archives require the host’s SoapySDR 0.8 runtime. Receivers handled through SoapySDR also need their hardware module; native drivers do not. Run sdrmm --doctor to check available drivers and devices.

Homebrew

On macOS, both packages come from the project’s tap:

brew tap newspicel/tap
brew install --cask sdrminusminus
brew install sdrmm

The cask installs the desktop application into /Applications. The formula installs the sdrmm server and Homebrew’s SoapySDR alongside it; brew services start sdrmm runs the server in the background and restarts it at login.

The formula also works on Homebrew for Linux. It installs the same portable binary published on the releases page, not a source build.

Nix

On NixOS or another Linux system with flakes enabled, install the Tauri desktop application directly from GitHub:

nix --extra-experimental-features 'nix-command flakes' \
  profile install github:Newspicel/sdrminusminus
sdrmm-desktop

The flake supports x86_64 and aarch64 Linux and exposes sdrmm-desktop, sdrmm, and default packages for each system. From a checkout, the following creates result/bin/sdrmm-desktop:

nix --extra-experimental-features 'nix-command flakes' build

The package links to Nixpkgs’ SoapySDR core and bundles no SoapySDR hardware modules. On NixOS, select the modules and device permissions in your system configuration. For example, with this repository declared as the sdrminusminus flake input:

environment.systemPackages = [
  (inputs.sdrminusminus.packages.${pkgs.stdenv.hostPlatform.system}.sdrmm.override {
    soapyPlugins = with pkgs; [
      soapyrtlsdr
      soapyhackrf
    ];
  })
];

hardware.rtl-sdr.enable = true;
hardware.hackrf.enable = true;
users.users.your-user.extraGroups = [ "plugdev" ];

Remove whichever module and hardware option you do not need. The selected plugins remain separate Nix store packages managed by NixOS; the application wrapper only points SoapySDR at them.

Container

The published container includes the web interface, SoapySDR, and the supported open-source hardware modules:

docker run --rm \
  -p 8080:8080 \
  -v sdrmm-data:/data \
  --device /dev/bus/usb:/dev/bus/usb \
  ghcr.io/newspicel/sdrminusminus:latest

On Linux, USB access still depends on host udev permissions. The repository’s docker-compose.yml also includes a device cgroup rule that keeps replugged USB devices accessible. See Containers and remote radios for a durable setup.

Stable and nightly builds

Stable releases use semantic versions and are suitable for persistent installations. The rolling nightly release is rebuilt from main when it changes. Nightlies use a date version and should be treated as prereleases.

Desktop builds check stable releases for updates at startup. Nightly releases are not offered as updates to stable installations.

Build from source

To contribute, choose a custom set of backends, or package another platform, follow Build and test.

Next step

Every installation includes a virtual signal source. Continue with Your first receiver before connecting hardware.

Your first receiver

Build an NFM receiver using the built-in signal generator. You will see its spectrum and hear a test tone without an SDR or antenna.

1. Open the signal generator

Start sdr– and open its interface. A fresh installation creates a workspace with three nodes:

  • a Device waiting for a radio;
  • a Scope already connected to the Device’s IQ output;
  • a Speaker waiting for channel audio.

On the Device node, choose Signal Generator (virtual). The device opens immediately and the Scope begins drawing a synthetic spectrum and waterfall.

If you do not see the starter nodes, create them from + Node. Draw a wire from the Device’s IQ port to the Scope’s IQ port.

2. Add a channel

Choose + Node, search for NFM, and add an NFM channel. Connect the nodes from left to right:

Device IQ → NFM IQ
NFM audio → Speaker audio

The patch applies as you work. If a channel says it has not been created, press Apply patch on that node.

Set the NFM channel to 300 kHz above the radio’s centre. The generator places an NFM carrier there with a 1 kHz audio tone.

3. Start audio

Use the Speaker node’s control. Your browser may require a click before it permits audio playback. Adjust the channel squelch if the tone stays muted.

4. Explore the interface

Try these next:

  • Drag the channel marker across the Scope to change its frequency.
  • Use the Device dial to retune the whole receiver.
  • Press [ or ] to change the tuning step, then use the arrow keys to tune.
  • Select a node and press p to pin its face to the Rack view.
  • Open Library → Templates to inspect ready-made FM, airband, ADS-B, ACARS, AIS, APRS, pager, PMR446, digital voice, ISM, and HF setups. Templates that require real off-air traffic still configure the signal generator, but their decoders will remain quiet.
  • Add a Recorder and wire the Device IQ output into it to create a short SigMF recording.

The ? button in the top-right corner opens the complete keyboard reference.

Move to real hardware

On the Device node, choose Forget this radio, then select the attached receiver. Device controls are built from the capabilities reported by its driver, so gain stages, antennas, sample rates, bandwidths, and advanced settings vary by model.

Run sdrmm --doctor or press Check hardware on an unbound Device node if your receiver is missing. The hardware guide covers supported modules and USB setup.

Understand the workspace

A workspace saves your receiver setup: nodes, connections, positions, rack layout, and regional band plan.

Patch and rack

The Patch view is where you build and troubleshoot signal flow. Ports are typed, so the canvas only permits meaningful connections: IQ feeds channels, scopes, and recorders; audio feeds a speaker; decoder events feed maps, readouts, logs, and exports; scanner control drives a device.

The Rack view collects the controls and displays you use most. Select a node and press p to pin or unpin it. Moving or resizing a face on the rack does not change its signal connections.

Node types

GroupNodesRole
SourcesDevice, GPS positionRadio IQ or a live station position
DecodersAM, NFM, WFM, SSB, ADS-B, DMR, and the rest of the channel catalogSelect and process one signal from device IQ
ToolsArray, Direction finder, Passive radar, Combiner, Scanner, Signal hunt, DMR trunk, Event filter, TriangulationControl radios, process arrays, or filter decoder events
OutputsScope, Map, Signal survey, Readout, Decoder log, Video, Speaker, Recorder, Audio recorder, Baseband recorder, Time machine, Network IQ, ExportDisplay, play, record, or export signals and events

The server supplies the node palette and channel catalog, so the interface shows the options available in the running build.

Live position wiring

Add a GPS position node and pick a source from its tabs. Sources read by the server use hardware or network endpoints reachable from the server machine. Browser location uses the device displaying the UI.

TabSource
ReceiverA detected serial NMEA receiver, searchable by path or device name, or a serial device path typed into Path
NetworkA gpsd JSON endpoint; default 127.0.0.1:2947
FixedFixed latitude and longitude
This deviceBrowser or desktop WebView location, where available

For serial sources, configure baud rate and maximum published update rate after selecting the receiver. The node validates GGA and RMC sentences. The update limit controls publication of fixes; NMEA receivers send sentences without polling. Browser location requests continuous high-accuracy updates. Forget source returns to the source picker.

Connect position to the nodes that need it:

  • ADS-B uses it as the local CPR position reference.
  • Map shows the station position, recent route, and visited-location heatmap.
  • Recorder writes latitude, longitude, altitude, and fix time into SigMF capture segments.

One position output can feed several nodes. The GPS display also shows a six-character Maidenhead locator. When a source loses its fix, the node reports why and consumers stop using the stale coordinate. Serial and gpsd sources reconnect automatically.

Drive a signal survey

Add a Signal survey display, wire one Device IQ output and one GPS position output into it, then choose an offset inside the incoming IQ span and a measurement width. The −25k, −5k, +5k, and +25k controls move the measured slice relative to the IQ centre; they do not retune the radio. Start the survey only after the face reports both a spectrum level and a GPS fix.

The map records the peak spectrum level in that width at each new fix. Readings are in dBFS, not dBm: keep receiver gain, antenna, cable, and measurement width unchanged if you want locations to be comparable. Nearby fixes are combined into roughly ten-metre cells, so time spent stationary does not make one place appear stronger. Pause before changing the radio setup, and export the current cells as CSV when the drive is complete.

Device identity and reconnection

A device node stores a durable hardware reference, not a temporary engine number. If you unplug a named receiver, the node stays in place with its wires and settings intact. Plug the same receiver back in and the workspace binds to it again; it does not silently substitute another device.

Forget this radio releases the device and clears that durable reference while preserving the node and its wires.

Applying a patch

Editing the graph saves the desired workspace. Applying reconciles that graph with live engine state: it opens attached devices, restores their settings, creates or updates channels, and removes live objects that no longer belong to the active patch.

Most edits apply automatically. An explicit Apply patch button appears when the desired graph and live state differ in a way that needs your attention.

Multiple workspaces and clients

The active workspace lives on the server, so every connected browser sees the same receiver state. Changing the active workspace, tuning a radio, or applying a template affects other clients too. Use separate workspaces for different activities, not as private per-browser tabs.

Only one client can edit a particular saved revision successfully. If two clients change the same workspace at once, the server reports a conflict rather than silently overwriting one operator’s layout.

Radios and hardware

sdr– opens a receiver in one of four ways: a built-in driver, a bundled SoapySDR module, a network protocol, or a virtual source. This page lists what each one supports and how to get a radio working.

Built-in drivers

Standard builds include native drivers for RTL-SDR, HackRF, SDRplay RSP, and Dragon Labs CR-8. These drivers do not require SoapySDR modules. Custom builds can omit them through feature flags.

ReceiverExtra software
RTL-SDRnone
HackRFnone
SDRplay RSP1, RSP1A, RSP1B, RSP2, RSPduo, RSPdx, RSPdx-R2SDRplay API 3.15 or newer, see SDRplay
Dragon Labs CR-8the vendor CR-8 library, see Dragon Labs CR-8

If a host SoapyRTLSDR, SoapyHackRF or SoapySDRPlay3 module is installed, it is skipped for these receivers so that one radio is never listed twice.

SoapySDR modules

Desktop installers and containers ship a private SoapySDR 0.8.1 runtime with these modules:

ReceiverModule
Airspy and Airspy HF+SoapyAirspy / SoapyAirspyHF
bladeRFSoapyBladeRF
LimeSDRSoapyLMS7
PlutoSDR and libiio devicesSoapyPlutoSDR
Remote Soapy serverSoapyRemote

The exact versions are pinned in packaging/soapy/environment.yml. UHD is not bundled because of its size. Other modules may work if they match the SoapySDR 0.8 module ABI, but they are not part of the release test matrix; a module built against a different SoapySDR generation is refused and logged rather than loaded.

Bundled installations use their own modules unless you add an explicit search path. Portable archives and source builds use the host’s SoapySDR installation. Native drivers are independent of this module selection.

To load a module the bundle does not carry, point SDRMM_SOAPY_MODULE_PATH at the directory holding it. Those directories are searched before the bundled ones.

Network receivers

On an unbound Device node, open the Network tab and enter a hostname or address. Ports default to 1234 for rtl_tcp and 5555 for SpyServer. Both protocols are built in and work without SoapySDR.

SoapyRemote is a separate path: a host running SoapySDRServer is discovered automatically and appears in the normal device list, so use that instead of the network form.

Virtual sources

Every build includes sources that need no hardware: a signal generator, a four-lane coherent array, a 2×2 transceiver, a half-duplex 1×1 transceiver, and playback of any SigMF recording in the library. Your first receiver builds a working receiver on the signal generator.

Check the installation

Run the diagnostic report before opening a radio:

sdrmm --doctor

It lists compiled backends, the SoapySDR core version, module search paths, discovered modules and devices, data paths, and Linux USB permission checks. The same report is available from Hardware not showing up? on an unbound Device node.

On a host SoapySDR installation, its own utility is also worth running:

SoapySDRUtil --info
SoapySDRUtil --find
SoapySDRUtil --probe="driver=airspy"

For a receiver using SoapySDR, fix discovery or permission errors here before starting sdr–. Use sdrmm --doctor for receivers handled by native drivers.

How radios are discovered

SoapySDR discovery runs in a short-lived child process because vendor modules can crash or hang while probing hardware. A failed probe produces a warning without terminating the application.

Discovery runs when attached USB devices change and once a minute to find network radios. For driver debugging, SDRMM_SOAPY_PROBE=in-process runs discovery in the application process.

Linux USB permissions

sdr– does not need root as long as the receiver’s udev rules are installed. On Debian-derived systems the driver package usually installs them. After adding or changing a rule, reload udev or reconnect the device.

In containers, the host rules still decide whether the unprivileged container user can open the USB node. Passing /dev/bus/usb is necessary but does not override its permissions. See Containers and remote radios for a Compose example that also survives reconnects.

Device controls

The device face is generated from the capabilities and setting metadata the driver reports, so it differs by model. A radio may expose:

  • separate RX and TX streams;
  • device-wide or per-stream tuning;
  • sample rates as a menu, as continuous windows, or as both;
  • analog bandwidths, likewise;
  • antennas, gain stages, AGC, and clock and time sources;
  • driver-specific booleans, enums, ranges, and text settings.

Changing a setting that affects capabilities makes sdr– re-read the device before validating the rest. RTL-SDR direct sampling is one example: it changes the available frequency range.

RTL-SDR

The built-in driver exposes:

SettingEffect
TUNERthe tuner gain stage
ppmcrystal frequency correction
bias_teephantom power on the antenna port
agcR82xx tuner AGC
direct_samplingoff, i, or q

Gain. The slider uses the tuner’s actual gain table. Values from the API or saved settings are rounded to the nearest supported entry; for example, an R820T request for 20 dB returns 19.7 dB.

Sample rate. The RTL2832U supports 225–300 kHz and 900 kHz–3.2 MHz. Rates in the gap are rejected. The picker also offers a menu of common rates.

IF filter. The R82xx driver exposes a 0–8 MHz setting. Use 0 for automatic bandwidth matched to the sample rate.

Direct sampling. This is available except on RTL-SDR Blog V4 boards. The V4 uses an upconverter for HF; tune below 28.8 MHz without changing the direct-sampling setting.

HackRF

Three gain stages and one switch:

SettingRange
LNA8 dB steps
VGA2 dB steps
AMPthe switched +14 dB RF amplifier, rendered as a switch
bias_teephantom power on the antenna port

AMP appears as a switch and contributes to the displayed total gain.

SDRplay

RSP receivers use a driver built into sdr–, with no SoapySDR module involved. That driver needs SDRplay’s own API, which is licensed for use with genuine SDRplay hardware and cannot be redistributed, so you install it yourself:

  1. Download and install the API from SDRplay. Version 3.15 or newer is required.
  2. Plug in the RSP. It appears in the device list.

sdr– loads the vendor library at runtime from the install location, usually /usr/local/lib on Linux and macOS or C:\Program Files\SDRplay\API on Windows. The library is not bundled. Without it, no RSP appears in the device list. sdrmm --doctor reports the result under SDRplay API.

The API also runs a background service (sdrplay_apiService). If the service is stopped, the doctor check reports that the API is not responding even though it is installed. Start the service and retry.

Gain

SDRplay hardware is specified in gain reduction. sdr– presents both stages as gain, so higher is always more signal:

  • RF — the LNA state, in dB of gain relative to that band’s weakest state. The available steps change with frequency, the selected port and HDR mode, so the range is re-read whenever tuning moves to another band.
  • IF — 0 to 39 dB, the inverse of the API’s 20–59 dB IF gain reduction.

AGC controls the IF stage. With AGC enabled, the IF slider sets the starting point and the setpoint extra sets the target level in dBFS.

Sample rates

Single-tuner modes sample the ADC between 2 and 10.66 MHz. Rates below 2 MHz are reached by decimating a legal ADC rate, so anything from 62.5 kHz to 10.66 MHz is available.

RSPduo

The RSPduo appears once per operating mode it can currently offer: Tuner 1, Tuner 2, Dual Tuner, Master and Slave. The chosen mode is part of the stored device identity, so a saved node comes back in the same mode. Dual Tuner gives one device with two independently tuned streams.

Dual Tuner, Master and Slave run the ADC at a fixed 6 MHz with a 1.62 MHz IF. Their sample rates are therefore 2 MHz and each halving below it down to 62.5 kHz, with analog bandwidth capped at 1.536 MHz. A Slave waits for its master application to start; if no master appears, starting the stream reports that it is still waiting. The master owns the clock, so a slave cannot change the sample rate beyond its own decimation and cannot apply a ppm correction.

If another application already holds the RSPduo, only the modes still free are listed.

Licensing

The interface to the vendor library is written in Rust from the public SDRplay API specification, whose legal notice grants a royalty-free licence to use the information in it to design software that uses SDRplay receivers. No SDRplay source, header or binary is copied into this project or shipped with it, and the gain tables above come from the same document.

Dragon Labs CR-8

The CR-8 provides eight phase_coherent lanes sharing a clock and synthesizer. Add one Device node and use iq through iq8; no Array node is needed. These lanes support calibration, direction finding, beamforming, and passive radar.

The vendor library is loaded at runtime and must be installed separately. Run sdrmm --doctor to check whether it loaded. Without it, no CR-8 is discovered.

SettingBehaviour
FrequencyAll eight channels are tuned together, in one coherent call
Sample rateFixed at 12.5 MS/s; any other rate is refused
GainLNA, mixer and VGA, settable per channel
Clock sourceThe on-board oscillator, or a 10 MHz reference on the external input

If the library is somewhere the loader will not look, point SDRMM_DLCR_LIBRARY at it.

The reported CR-8 tuning range comes from its documentation; the SDK does not expose that range.

Before an unattended deployment

Test the exact packaged build against the exact radio:

  1. Run sdrmm --doctor and save the module versions.
  2. On a host runtime, probe the device with SoapySDRUtil.
  3. Stream for at least 30 minutes and watch the Device overrun counter.
  4. Unplug and reconnect once, and confirm the workspace binds to the same radio again.
  5. Exercise tuning, gain, AGC, bandwidth, antenna, and the advertised advanced settings.
  6. Make a short recording and replay it.

sdr– reports the TX capabilities of transmit-capable radios, but the transmit workflow is not available yet. Validate driver TX behaviour only in a shielded, attenuated, legally authorized bench setup.

Channels and decoding

A channel listens on one frequency. That frequency belongs to the channel, not to the radio feeding it: retuning the radio leaves every channel where it was. A channel whose frequency falls outside what the radio is sampling stays set up and goes quiet, and its face offers to tune the radio over it.

Add a channel

Add a channel from + Node, then wire the Device’s IQ output into the channel’s IQ input. Connect the outputs you need:

Channel outputConnect toResult
audioSpeakerBrowser audio
eventsReadoutAccumulated state, such as a station or aircraft table
eventsDecoder logStored, filterable message history
eventsMapPositions from ADS-B, AIS, APRS and other locating decoders
eventsExportCSV or JSON download of stored rows
videoVideoATV frames, and an SSTV picture as it scans out

Channel catalog

The node palette lists every channel type under Decoders, whether it produces audio or events. The server reports the exact catalog for the running build; this is the current list.

GroupChannelsMaturity
Analog voiceAM, NFM, SSB, WFM (broadcast, with stereo and RDS)tested on air
Digital voiceDMRtested on air
Digital voiceFreeDV 1600tested on air
Digital voiceD-STAR, System Fusion, NXDN, P25 Phase 1, dPMR, M17fixture-only
AviationADS-B (1090ES)tested on air
AviationACARS, VDL Mode 2, HFDL, Inmarsat Classic Aerofixture-only
AviationVOR, ILS localizer / glideslopeexperimental
MarineAIS, NAVTEX, Digital Selective Calling, Inmarsat STD-C / EGCfixture-only
Amateur data and HFAPRS / AX.25, RTTY, PSK (31, 63, 125, 250 baud), Morse (CW), CW skimmer, FT8, FT4, WSPRfixture-only
Paging and telemetryPOCSAGtested on air
Paging and telemetryFLEX, ERMES, Selcall (CCIR/ZVEI), Sub-GHz OOK/FSK frames, ISM sensors, radio clocks (DCF77, WWVB, MSF, JJY)fixture-only
VideoATV, SSTVfixture-only
Wideband digitalDAB / DAB+, DATV (DVB-S / S2), DRM30 / DRM+experimental
UtilitySignal identifier, Iridium bursts, DECT base station surveyfixture-only
UtilityGNSS lab (GPS L1 C/A)experimental

Coverage varies by protocol. The catalog lists implemented signal paths, but optional services, trunking variants, and vendor extensions may be unsupported. Check the mode-specific limits below.

What the maturity labels mean

LabelEvidence
tested on airVerified with a real transmitter through the receiver and decoder integration
fixture-onlyTested with generated IQ and, where available, published reference vectors; this integration has not been verified on air
experimentalPartial acquisition, decoding, or measurement support; not an operational receiver for the full service

Generated fixtures catch decoding errors, but do not establish tolerance to transmitter drift, keying transients, adjacent-channel interference, or multipath. Most decoders have only this coverage.

Committed recordings add regression coverage for DMR, ADS-B, FreeDV 1600, and a busy FT8 slot. Their origins and expected output are listed in the fixture library. A recording test does not necessarily verify the whole live receive path, and a maturity label applies only to the services tested.

Some decoders also use worked examples from their standards, including ADS-B frames, APRS compressed positions, CCIR 476 characters, and radio-clock minutes. Iridium tests use an off-air bit sequence with a synthetic waveform, which tests real framing but not real RF conditions.

VDL Mode 2, HFDL, Inmarsat Classic Aero, Inmarsat STD-C, and Digital Selective Calling use decoders from xng. Their labels describe the sdr– integration, separately from upstream testing.

ISM sensors

A Sub-GHz channel decodes known sensor payloads and displays raw frames for other signals. Supported devices are grouped by pulse coding:

CodingDevices
Pulse positionNexus-T/TH, Rubicson (also Solight TE44, EMOS E0107T), Acurite 609TXC, Acurite 606TX, Prologue-TH, inFactory-TH, Kedsum-TH, Springfield soil probe
Pulse widthLaCrosse TX141TH-Bv2, Fine Offset WH2, Auriol HG02832, Geevon TX16-3, WS2032 weather mast, EMOS E6016 rain gauge, Rubicson 48942 pool, WT0124 pool, Opus XT300 soil probe
ManchesterAmbient Weather F007TH
Pulse code (FSK)Ambient Weather WH31E, Renault TPMS, Toyota TPMS
Differential ManchesterWT450-TH

Readings can include temperature, humidity, soil moisture, wind speed and direction, rainfall, tyre pressure, and power. Renault TPMS adds Manchester coding after framing; Toyota TPMS adds differential Manchester.

The decoder checks pulse timings and the device’s checksum, digest, or parity before reporting a reading. Unrecognised bursts remain available in the raw timing view. FSK sensors use bit periods of 55–58 µs; the default minimum pulse width admits these signals.

Pulse slicing, payload layouts, validation rules, and CRC/LFSR digest routines follow rtl_433, licensed GPL-2.0-or-later.

Experimental mode limits

ModeAvailable outputMissing or limited functionality
DAB / DAB+FIC and MSC decoding, CRC-checked DAB+ access unitsNo audio codec or playback
DATVDVB-S/S2 transport packets and programme tables, or generic-stream datagramsNo audio or video codec output
DRM30 / DRM+Acquisition, lock, SNR, and frequency errorNo FAC, SDC, or MSC decoding; no service labels or media
GNSS labGPS L1 C/A acquisition and NAV telemetryNo position solution
VOR / ILSRadial or difference in depth of modulationTested only against analytically generated signals

To add on-air coverage, contribute a short IQ capture restricted to the relevant band, with its expected decoded output. See Build and test and the contribution guide.

Pager text

POCSAG uses seven-bit characters. Some German networks use DIN 66003, which replaces ASCII brackets and related punctuation with umlauts and ß.

sdr– applies this mapping when the affected character appears inside a word beside a lowercase letter: M}nchen becomes München, and Stra~e becomes Straße. Otherwise it keeps ASCII, so [ALARM] retains its brackets. Entirely uppercase pages remain ASCII. There is no manual setting.

Sample rate and passband

A channel’s occupied band must fit inside its source device’s current passband. If it does not, move the channel closer to center, raise the device sample rate, or retune the device.

Most channels resample device IQ to their processing rate, provided their occupied band fits in the device passband. The following channels process samples at the device rate and require:

ChannelRequired device rate
ADS-B2–4 MS/s
ATV2–20 MS/s
GNSS lab2.048 MS/s

The channel face says so when the current rate cannot work, and offers a compatible one.

Otherwise use the lowest rate that covers the signals you need. Higher rates increase USB traffic, FFT work and CPU load without improving a narrow channel.

Tuning and squelch

Tune a channel by editing its frequency, dragging its marker on a connected Scope, or using the keyboard while the channel is selected. The field takes megahertz; the −25k, −5k, +5k and +25k buttons step it.

The keyboard button beside the field takes a typed frequency. A bare number is read as megahertz; a kHz, MHz or GHz suffix is honoured. The span the radio currently hears is shown below the field — a frequency outside it is accepted, and the channel waits there silently until the radio covers it.

The lock beside the dial holds the channel on its frequency. While it is held, the dial, typed entry, and Scope marker refuse to move the channel; the radio feeding it can still be retuned. The Device node carries the same lock for the radio itself.

A channel node that is not wired to anything yet can still be given a frequency; it is held against the node and applied the moment a radio carries it. A radio nobody has tuned by hand opens over the channels wired into it.

Every channel can gate what it decodes with squelch. Off passes everything through. Manual opens above a level you set; a lower threshold opens more easily.

Auto opens a chosen number of decibels above the channel’s measured noise floor, with no fixed level needed. The level meter under the dial marks where the gate opens.

The channel learns the noise floor during quiet periods. A continuous signal may be mistaken for the floor, requiring a stronger signal to open the gate. Once the gate opens, the floor cannot rise and suppress a long transmission.

Switching from Auto back to Manual restores the level you last set by hand.

NFM adds tone squelch:

  • Detect reports any recognized CTCSS tone or DCS code without gating audio.
  • CTCSS opens only for a selected standard tone.
  • DCS opens only for a selected standard code.

Compander applies 2:1 audio expansion to receive signals transmitted with matching compression. Enable it only for a companded NFM link; ordinary NFM speech can become too quiet with expansion. The corresponding transmit setting applies compression. Expansion stops 20 dB below the reference level, and sub-audible tones are excluded from level tracking.

Audio processing

Audio channels share an Audio block. Processing is off by default except for AGC on AM and SSB. The stages run in the order shown below.

StageEffect and controls
BlankerRemoves IQ impulses before the channel filter. Lower thresholds remove more impulses but can also damage the wanted signal.
De-clickRemoves short audio impulses after demodulation. Detection compares each sample with the surrounding level and neighbours; width is set by mode.
PassbandSets low and high audio cutoffs. Narrow the range to the audio you need.
NotchesRemoves up to four selected frequencies, each with an adjustable width.
Auto notchSuppresses steady carriers without manual frequency selection.
DenoiseTracks the noise floor in each spectral bin and attenuates bins without a detected signal. Strength ranges from no attenuation at 0 to 20 dB at 100. Continuous carriers can be treated as noise.
AGCLevels audio. Slow suits SSB speech, fast suits tuning, and medium provides an intermediate response.

The blanker runs on IQ; the remaining stages run on audio. Removing impulses before filtering reduces the ringing they would otherwise cause.

Slow-scan television

An SSTV picture takes 36 seconds to four and a half minutes to receive, depending on mode. Tune to the SSB carrier; the channel processes the 1000–2600 Hz video subcarrier above it.

A transmission names its own mode in the VIS header that precedes it. Follow VIS, the default, reads that header and recognizes Robot 36 and 72, Martin M1 and M2, Scottie S1, S2 and DX, PD50, PD90, PD120 and PD180, and Wraase SC2-180. Pick a mode by hand when the header was missed or corrupted; the decoder then starts on any header it sees and scans it as the mode you chose.

Slant correction tracks each line’s sync pulse instead of free-running from the header, which keeps the picture upright when your sample clock and the transmitter’s disagree. Leave it on unless you are diagnosing the sync itself.

Keep unfinished pictures decides what happens when a transmission fades or is cut short. On, the lines that did arrive are kept; off, only a picture that scanned to its last line is.

Wire the channel’s video output into a Video node to watch a picture build up line by line. Every finished picture, and every kept partial, is also stored on the server as a PNG and listed in the channel’s own panel, so a picture that arrived while no browser was connected is still there. The store holds 24 hours of pictures, capped at 512 of them.

Surveying a DECT network

The dect channel surveys base stations on one DECT carrier. It reads the 64-bit A-field in each burst for identity, configuration, and authentication or ciphering signalling. It does not decode the B-field containing call audio and user data.

A DECT carrier is 1.728 MHz wide and the channel runs at 2.304 MHz, so the receiver needs at least that much bandwidth and must reach the band: 1880–1900 MHz in Europe, 1920–1930 MHz in the US. An RTL-SDR tops out below the band and cannot be used; a HackRF or an SDRplay can.

Set Band so carrier numbers resolve to frequencies, and set Side to Base if you only want the fixed part, Handset for portables, or Both. Carrier 0 is the highest frequency in the European band (1897.344 MHz) and they count downwards in 1.728 MHz steps to carrier 9 at 1881.792 MHz; the US band counts upwards from 1921.536 MHz.

Each base station transmits a dummy bearer once per 10 ms frame in a fixed slot, cycling through the identity and system-information messages. The decoder groups bursts by their slot timing, so several base stations sharing one carrier stay apart, and folds each one into a single record:

  • RFPI — the 40-bit Radio Fixed Part Identity, broadcast on the Nt channel. It splits into the access rights class (A residential, B private multi-cell, C public, D GSM/UMTS, E direct), the manufacturer, installer or operator code, the fixed part number and sub-number, and the radio fixed part number that separates cells within one system. Class C and D encode single-cell versus multi-cell in the low bit of the RPN.
  • System information — the carrier the base is on and its frequency, which slot pair it uses, how many transceivers it has, which of the ten carriers it says are available, and its primary scan carrier number.
  • Capabilities — the fixed part capabilities broadcast, decoded bit by bit: slot types, frequency control, handover, the connectionless services, and the higher-layer services.
  • Security — whether the base advertises standard authentication (DSAA) and standard ciphering (DSC), and, separately, whether encryption was actually negotiated on the air. MAC encryption-control messages are followed through request, confirm and grant, so a bearer shows as encrypted only once the grant is seen. A cipher key index is reported when the base uses the keyed variant.
  • Handsets — the PMIDs seen in encryption handshakes, plus the FMID of the fixed part.

Each A-field must pass its R-CRC check. Burst and error counts appear beside each station.

Advertised ciphering support does not establish whether a call uses encryption. The reported encryption state follows observed request, confirm, and grant messages; missing signalling is not proof that a call is unencrypted.

Following a DMR trunk system

Add a DMR trunk system node, connect a Device’s iq output, and enter the control-channel frequency in MHz. Select a system type or use auto-detect. The node manages its own DMR decoders.

SystemChannel discovery
Tier III, including Capacity MaxLearns logical channel definitions and opens traffic channels named in voice grants
Capacity PlusUses Repeater outputs, or Search to find carriers that announce and follow the same rest-channel changes; follows both timeslots
Hytera XPTUses the same approach as Capacity Plus with XPT signalling

Following runs on the server even when no browser is connected. Traffic channels must fit in the source radio’s passband. If a grant falls outside it, the node reports the failure. Increase the sample rate or retune to include the required frequencies.

Enable Record calls to buffer completed calls and their audio in memory. Encrypted calls retain metadata only. Disable it to follow traffic without buffering audio.

Where decoder output goes

Decoder events are typed on the server and timestamped with source and frequency information. Choose the destination that matches the job:

NodeUse it for
ReadoutChanging state, such as RDS text or a table of tracked aircraft
Decoder logIndependent messages and frames, stored in SQLite for filtering and review
MapRecent position tracks from locating decoders
ExportDownloading the stored rows wired into it

Decoder log history is bounded, so a busy unattended receiver cannot grow the database forever.

Pictures are not decoder-log rows. A completed SSTV picture writes one line to the log recording what arrived, while the pixels go to the picture store and are served from GET /api/images.

Coherent arrays

An array processes samples from several antennas together. Direction finding and beamforming need stable phase relationships; passive radar needs aligned sample timing. The hardware’s shared clocks determine which operations are available.

TierShared hardwareSupported measurements
phase_coherentReference clock and synthesizerBearings, beamforming, combining, and passive radar
time_syncReference clockPassive radar; phase-dependent operations also need a calibration reference
noneNo shared referenceIndependent reception only

Retuning a time_sync array changes the relative tuner phases. Use an injected noise source or a known pilot carrier to calibrate them. Without that reference, the direction finder reports phase unknown and produces no bearings. Receivers without a shared clock drift apart and cannot form a coherent array.

Radios that are already an array

For a multi-lane receiver such as a Dragon Labs CR-8, an RSPduo in dual-tuner mode, or a multi-channel SoapySDR device, add one Device node. Connect its iq, iq2, iq3… outputs to the processing nodes. The driver reports the receiver’s coherence tier.

Radios you wired together yourself

Use an Array node for separate receivers connected to a shared clock.

  1. Add a Device node for each radio and select its receiver.
  2. Set the radios to the same sample rate. For shared tuning, their centre frequencies must match.
  3. Add an Array node and connect each Device’s iq output to an array input. A spare input appears as you add members.
  4. Set Wired as to match the physical connections: shared clock, or shared clock and LO.

Input order determines antenna element numbering. Move the wires if that order is wrong. Connect the Array’s output lanes to a direction finder, combiner, channel, or recorder.

Tuning and membership

Once connected, change frequency and sample rate through the Array node to keep its members consistent. Independently tuned arrays have a frequency control for each lane. Disconnect the array before scanning or hunting.

Each Device node retains its radio and existing channels, scopes, and recordings. Removing the Array leaves those running. Removing a member removes the dependent array. If a member disconnects, the array reports a fault and reconnects its streams when all members recover.

Use fixed gain on each element. AGC changes the amplitude relationship and invalidates calibration. Fixed gains may differ: calibration measures and corrects each lane’s amplitude and phase.

Calibration

Press Calibrate on the coherent node. Calibration measures a delay and complex weight for each lane, then applies those corrections before processing.

Cal sourceWhen to use it
SignalA strong signal received by every element
NoiseA noise burst injected through a splitter for bench calibration

A time_sync array needs injected noise or a specified pilot frequency to solve relative phase. On phase_coherent hardware, calibration corrects the additional differences from cabling.

The readout shows solved, still solving, or phase unknown. Phase unknown means the hardware tier and calibration source do not provide enough information for a bearing.

Combining antennas

A Combiner sums the lanes of one coherent source and sends the result to a beam lane. Connect an ordinary channel to that lane to decode the combined signal.

ModeEffect
CombineAligns and sums antenna signals. Two antennas can improve SNR by about 3 dB under suitable conditions.
CancelUses the other antennas as noise references and subtracts their contribution from the first.

For cancellation, use the first antenna for the wanted signal and the others to receive the local noise source. Both modes calculate weights from the covariance between lanes and require known relative phase. A time_sync array therefore needs a pilot or noise reference.

Direction finding

A Direction finder estimates a signal’s arrival direction from phase differences across a coherent array. It displays a bearing, confidence, and angular response so you can see competing peaks.

Wire one up

  1. Add a multi-lane Device, or an Array node for separate radios sharing a clock.
  2. Add a Direction finder. Set Geometry to your antenna layout: a circle with a radius, a line with element spacing, or explicit element positions. Set Elements to the antenna count.
  3. Connect every source lane to the corresponding iq, iq2, iq3… input. All lanes must come from the same Device or Array. Applying an incomplete set of connections reports an error.
  4. Connect a GPS position source to position to place bearings on a map or use triangulation.
  5. Set Offset and Bandwidth to select the signal within the source’s tuned span.

Algorithm

AlgorithmBehaviour
BeamformerBroader angular response; useful as a baseline with limited covariance data
MUSICSharper peaks; depends on an accurate source count

Sources sets the number of arrivals MUSIC should assume. Start with one for a single source.

The compass and what it is telling you

The compass shows the angular response, selected bearing, and confidence. A strip below it shows calibration quality for each lane. Bearings run clockwise from north at 0°.

When calibration reports phase unknown, the node neither displays nor publishes a bearing. Check the array’s clock connections and calibration reference.

The beam output

The beam output sums the elements toward a selected bearing. Connect it to a channel to listen in that direction.

BeamBehaviour
Follow bearingTracks the current estimated bearing
Fixed azimuthHolds a chosen direction

Switching to fixed azimuth starts at the beam’s current direction.

Crossing bearings from several finders

Add a Triangulation node and connect each Direction finder’s events output. Bearings from different positions constrain the transmitter’s estimated location.

Each finder needs its own position source. Use GPS for a moving receiver, or a GPS position node with fixed latitude and longitude for a stationary one.

The Triangulation node shows the position estimate, error ellipse, guidance, and the age of each finder’s latest report. Clear resets the accumulated estimate.

A Direction finder works without triangulation, but then provides no position estimate, driving guidance, or event announcing a converged fix.

On the map

Connect a Direction finder’s events output to a Map to draw bearing rays that fade with age. Connect Triangulation events to add the combined position estimate, uncertainty ellipse, contributing stations, and suggested next waypoint.

Guidance

When the estimate has a long, narrow error ellipse, guidance suggests moving across the bearing to improve the intersection angle. Once the estimate converges, it switches to approach.

The first converged fix publishes a decoded event. Connected webhook, MQTT, or Matrix outputs can forward it. Use field mode for the phone interface and navigation.

Passive radar

Passive radar detects reflections of an existing transmitter, usually a broadcast station. A reference antenna receives the transmitter directly; a surveillance antenna receives the area of interest. Comparing the signals gives the echo’s delay and Doppler shift.

The two receiver lanes must share a sample clock. A time-synced array is sufficient; relative phase calibration is not required.

Wire one up

  1. Set up a Device or Array with at least two time-synced lanes.
  2. Add a Passive radar node.
  3. Connect the antenna aimed at the transmitter to ref and the surveillance antenna to surv.
  4. Connect a GPS position source to position if you want map output.

Processing and settings

StagePurpose
ECACancel the direct signal and zero-Doppler clutter in the surveillance lane
CAFCorrelate the remaining signal with the reference across delay and Doppler offsets
CFARDetect cells above a threshold calculated from their neighbourhood
ClusterMerge adjacent detections into one echo
TrackAssociate echoes across successive integrations

Integration sets the coherent processing interval. Longer intervals can reveal weaker echoes, but target motion during the interval can blur them. Range bins sets the delay extent of the surface. Doppler span sets the frequency-shift range searched.

Reading the surface

The display plots range against Doppler and marks detections. New detections have no target number. After repeated observations, the tracker assigns a number and retains it as the echo moves. A brief detection may be noise or an echo the tracker cannot confirm.

Echoes on the map

A detection measures bistatic range: the extra distance travelled by the reflected signal compared with the direct path. Possible target locations lie on an ellipse whose foci are the transmitter and receiver. A detection alone does not provide a target position or bearing.

Enable Transmitter and enter its latitude, longitude, and frequency. With the receiver position available, the map can draw the ellipse for each echo. Without transmitter coordinates, no ellipse is drawn.

Tracking operates in range and Doppler. The map does not track geographic target positions.

In the field

The Radar watch mission shows the range–Doppler display and tracked echoes on a phone. See field mode.

Field mode

Field mode is a phone interface for signal hunting, direction finding, and passive radar. It uses the active workspace on the same server. Build the workspace in the desktop interface, then open /field from a browser that can reach the server.

Get it onto a phone

Open Library → Field and scan the QR code with the phone’s camera. The link includes the server token. Field mode stores it and removes it from the address bar.

When the desktop browser uses localhost, the QR code offers a server LAN address that the phone can reach instead.

Missions

/field lists the missions available from nodes in the active workspace. Each mission controls one node.

MissionRequired nodeControls and displays
Fox huntSignal huntSignal level, rising/falling indication, variable-rate click track, start/stop
DF driveDirection finderCompass, guidance, and map
Radar watchPassive radarRange–Doppler surface and tracked echoes

Missions offer fullscreen mode and keep the screen awake where the browser supports it.

Driving to a signal

DF drive rotates the compass to the vehicle’s GPS course over ground. It does not use the phone’s compass sensor. Guidance suggests crossing the bearing while the estimate is uncertain, then approaching it after convergence.

Guidance requires a connected Triangulation node. Without one, the bearing compass still works and the screen reports that guidance is unavailable.

Turn-by-turn

With a routing backend configured, DF drive shows a route, the next manoeuvre, and its distance. Spoken directions become available after the first touch interaction.

The mission requests a new route when you leave the route, the target moves, or guidance changes between crossing and approaching. It does not poll for routes on a timer.

Nav modeBehaviour
AutoRoute to a crossing waypoint until the fix converges, then to the estimate
DirectRoute to the current estimate
OffCompass and direction arrow only

If routing is unconfigured or unavailable, the mission reports the problem and uses heading guidance. Navigate in Maps opens the current target in the phone’s navigation app. Use it again when the target changes; the browser cannot update the foreground native app automatically.

See server configuration for backend options. The routing API key stays on the server.

Maps without internet

Place an archive named basemap.pmtiles beside the database to use an offline basemap. Otherwise, field mode uses the online map style. If neither is available, bearings, routes, and markers remain visible on a blank background.

Scanning

The Scanner node repeatedly retunes one device and measures activity across a list or range of frequencies. Use it to find intermittent signals that are easy to miss while parked on one channel.

Build a scanner

  1. Add a Scanner from + Node.
  2. Wire the Scanner’s control output into the Device’s control input.
  3. Enter the scan targets or ranges in the Scanner face.
  4. Choose the step, dwell time, threshold, and action.
  5. Start the scan.

The control wire represents ownership. While a scan runs, it owns the device center frequency and manual retuning is refused. Stop the scanner to return the dial to normal operation.

Configure the sweep

A range consists of a start frequency, end frequency, and step. Keep the step aligned with the channel spacing used by the service you are monitoring. A smaller step examines more frequencies but lengthens each sweep.

The dwell time controls how long the scanner observes each target. Digital bursts and weak squelched voice may need a longer dwell; strong continuous carriers can use a shorter one.

The activity threshold is measured from the device spectrum. Set it above the local noise floor, then adjust after watching several sweeps.

Scan actions

The scanner can continue through active signals or hold according to its configured action. Its live face reports the current frequency, progress, detected level, state, and any fault.

Scanning retunes the whole device, so channels attached to that device move with it. For a listening scanner, configure a channel on the radio’s own centre with the appropriate mode and connect it to a Speaker. For a fixed wideband task such as two-channel AIS, use normal channels instead of a retuning scanner.

Practical limits

The current scanner sweeps by retuning the receiver. It does not use firmware-assisted wideband sweep modes, and each retune needs time for the hardware and DSP path to settle. Scanning very large ranges is therefore best divided into smaller service-specific workspaces.

Propagation map

The Propagation map uses FT8, FT4, and WSPR decodes to show reception paths and estimate a lower bound on maximum usable frequency (MUF). It processes decoder events and adds no DSP load. The reflection points and MUF values are estimates based on a configurable ionospheric layer model.

Build one

  1. Add one or more FT8, FT4, or WSPR channels.
  2. Add a GPS position source and a Propagation map from + Node.
  3. Connect each channel’s events output to the map’s events input.
  4. Connect GPS position to the map’s position input.

A receiver position is required to calculate paths. For a stationary receiver without GPS, open the Fixed tab on the GPS position node and enter its latitude and longitude.

When opened, the map also loads the last six hours of decoder-log history for its connected channels.

What is plotted

A decode containing a Maidenhead grid square defines a path from your receiver to that square. The model divides the path into hops and estimates their reflection points. For a single hop, the reflection point is the midpoint.

Reflection points are grouped into Maidenhead squares. Each decode adds weight that decays with age: half remains after one Half-life, a quarter after two. Set the half-life between five minutes and twelve hours depending on how much history you want to see.

Messages without a grid square do not add a path. This includes signal reports, RRR, RR73, and 73, so many messages in an ongoing contact contribute no new map data.

Measured MUF

Receiving a signal at frequency f shows that its path supported that frequency at that time. The model scales this observation to a 3000 km reference hop:

MUF(3000) ≥ f × M(3000) / M(D / hops)

Here, D is path length and M is the obliquity factor (sec φ) for a thin reflecting layer over a spherical Earth. At the default layer height of 300 km, M(3000) is about 3.28. A 3000 km single-hop path therefore reports the received frequency; shorter hops scale it upward.

Read this as a model-dependent lower bound, not a measurement of the highest open band:

  • Paths shorter than 500 km contribute to activity but are excluded from MUF estimates. Ground-wave and near-vertical paths do not fit this calculation reliably.
  • The estimate depends on which bands you monitored. No 10 m decodes cannot establish that 10 m was closed. A value below the forecast does not by itself disprove the forecast.
  • The assumed layer height changes the result. Use 300 km for F2 estimates or 110 km when modelling sporadic-E.

Comparing against the ionosonde network

Enable Ionosondes to fetch sounding data from GIRO and INGV through the prop.kc2g.com feed. The server caches the response for fifteen minutes. The map shows each station’s MUF(3000 km) and compares each measured square with an inverse-distance interpolation of sounding sites within 3000 km. The footer gives the number of squares above the forecast and the median difference.

If the feed is unreachable, the map reports the reason and continues displaying local decodes. Disable Ionosondes to stop these feed requests. Basemap requests are separate.

Reading the map

LayerDisplay
ActivityEstimated reflection points, weighted by decode count and age
MUFOne labelled point per square showing its estimated MUF lower bound
PathsGreat-circle paths to stations, one per station and band, newest first

Paths is off by default to keep busy bands readable. The table below the map ranks squares by activity weight.

Recording and playback

sdr– records a device stream as a SigMF pair: metadata in .sigmf-meta and complex samples in .sigmf-data. The format preserves the center frequency, sample rate, timing, and capture details needed to process the IQ again.

Record IQ

  1. Add a Recorder node.
  2. Wire a Device IQ output into the Recorder IQ input.
  3. Start the radio, then press Record.
  4. Press Stop to finalize the files.

For a multi-stream device, the source port chooses which stream is recorded. The Recorder face shows elapsed time, bytes written, dropped samples, and write errors.

Stop the server cleanly when a recording is active. Graceful shutdown joins the writer and finalizes the pair; forcibly killing the process can leave an incomplete capture.

Record a channel’s audio

An Audio recorder saves channel audio after squelch, filtering, noise reduction, and AGC.

  1. Add an Audio recorder.
  2. Connect one or more channel audio outputs to its audio input.
  3. Press Record beside a channel to start its file, then Stop to finish it.

Each channel gets a separate 16-bit PCM WAV at 48 kHz. Closed squelch writes silence, preserving the timing of quiet periods. Recording continues through mode and device sample-rate changes; removing the channel finishes its file. WAV headers are updated during recording so an interrupted file remains playable up to the last finalized data.

Audio and device IQ recording operate independently. You can run both and stop either one separately.

Record a channel’s baseband

A Baseband recorder saves channel IQ after frequency translation and filtering, at the channel’s sample rate. This uses less storage than recording the full device bandwidth.

  1. Add a Baseband recorder.
  2. Connect one or more channel baseband outputs to its baseband input.
  3. Press Record beside a channel to start its SigMF pair, then Stop to finish it.

Each pair records the channel’s own frequency. The recording tap is before squelch, so closed squelch does not interrupt capture.

Completed pairs appear in the IQ recording library and can be opened as playback sources. A change that rebuilds the channel, including a mode or device sample-rate change, finishes the file because the recording cannot change sample rate mid-file. Removing the channel also finishes it.

The IQ time machine

A Time machine keeps recent device IQ in memory. Use it to capture a signal after hearing it.

  1. Add a Time machine and connect Device IQ. Optionally connect GPS position.
  2. Set the buffer duration and press Arm.
  3. Press Capture to write the buffered samples to a SigMF pair and continue recording live IQ.
  4. Press Stop to finish the pair and stay armed, or Disarm to release the buffer.

Memory use is seconds × sample rate × 8 bytes. The node shows the required memory and the maximum duration allowed by the server’s 1 GiB limit.

The sample rate is locked while armed. Retuning remains available and creates a new SigMF capture segment. The recording’s first timestamp belongs to the oldest buffered sample.

Storage

The headless server stores recordings in the platform data directory by default, under sdrmm/recordings. Set an explicit location with:

sdrmm --recordings-dir /srv/sdrmm/recordings

The container fixes this path at /data/recordings, so persist /data with a volume. The server reconciles the database index with valid SigMF pairs on disk when listing recordings; the files remain the source of truth.

Tags and notes

Open Library → Recordings and press Annotate. Enter comma-separated tags and a note. Duplicate tags are merged, keeping the first spelling.

Annotations are stored in .sigmf-meta as sdrmm:tags and core:description. They stay with SigMF downloads and survive rebuilding the database index. Library search matches file names, tags, and notes; clicking a tag searches for it.

Download and export

Open Library → Recordings to inspect captures. A recording can be downloaded as:

  • its original SigMF archive;
  • a stereo floating-point WAV with I and Q as its two channels for tools such as HDSDR, SDR#, or Audacity. WAV keeps the samples but only part of the SigMF metadata.

Audio recordings appear under Channel audio in the same drawer and download as WAV files. They are stored in the audio subdirectory of the recordings directory and listed directly from disk.

Large downloads include a content length and stream from disk. If an export fails, the response is aborted instead of returning a silently truncated file.

Play a recording

In Library → Recordings, choose Open as source. sdr– adds a virtual playback Device to the canvas. Wire it to channels and displays exactly as you would a live receiver.

Playback is pinned to the capture’s center frequency and sample rate. The device transport lets you play, pause, stop, and seek without changing the recording. Reopening the same capture is useful for testing different channels, decoder settings, or graph layouts against identical IQ.

Decoder logs are separate

IQ recording saves raw device samples. Decoder logs save structured output such as messages, identifiers, and positions in SQLite. Wire decoder event outputs to a Decoder log and optionally an Export node when you need CSV or JSON rather than raw RF samples.

Network IQ export

Network IQ sends a live stream to another analysis program without first recording it.

  1. Add a Network IQ node.
  2. Connect one Device IQ output or one channel baseband output. For multi-stream radios, choose the lane to export. A node cannot accept both source types at once.
  3. Choose UDP or TCP, the sample encoding, and a host:port destination.
  4. Start the receiving tool first, then press Start export.

The face reports the exact sample rate and center frequency to enter in the receiver, plus sent bytes, datagram/write count, capture overruns, and writer errors. The sample rate is locked while the export is active because the raw stream has no in-band rate-change message. Retuning remains available; update the receiver’s center-frequency setting after a retune.

Channel baseband uses the same format at the channel’s lower sample rate, after frequency translation and filtering. This reduces network bandwidth when another tool only needs one signal. Each channel supports one export, independently of device-wide export.

Wire contract

The payload is unframed, interleaved I, Q, I, Q, ... in one of these encodings:

SettingComponentsBytes per complex sampleTypical receiver type
cf32_leIEEE-754 32-bit float, little-endian8GNU Radio Complex
ci16_lesigned 16-bit integer, little-endian4GNU Radio Short, then Interleaved Short to Complex
cu8unsigned 8-bit integer, zero at 127.52RTL-SDR-style byte IQ

The identifiers and byte layout follow the SigMF datatype definitions, which specify interleaved I-first complex samples. This is not itself a SigMF recording because a live stream has no SigMF metadata file; the node reports the rate and center frequency separately.

UDP uses payloads of at most 1,400 bytes, below a normal 1,500-byte Ethernet MTU after IP and UDP headers. Each datagram contains a whole number of complex samples. There is no sequence header, so the stream cannot reliably identify missing or reordered datagrams. sdr– reports loss before the socket, but cannot report network delivery loss. GNU Radio’s UDP Source should use header None, the matching data type, and a payload size of 1,400.

TCP connects outward to the destination and writes one continuous byte stream. The receiving program must be listening before export starts. TCP avoids datagram loss and reordering, but a receiver that cannot keep up eventually fills the bounded export queue; sdr– then stops the writer and reports the error.

Security

The API caller chooses the destination host and port, and an active export can send several megabits per second. Keep the server restricted to trusted callers. In particular, configure the shared token and appropriate network access controls whenever the HTTP server is reachable beyond the local desktop.

Protocol compatibility

This output is raw IQ. Configure the receiving program with the same encoding, sample rate, and centre frequency; the stream carries no timestamps, stream IDs, or radio metadata.

It is not a VITA 49 or DIFI stream. Receivers expecting either protocol need framing and context that this output does not provide.

It is also separate from rtl_tcp, which lets a client control a remote radio’s tuning, sample rate, and gain. sdr– supports rtl_tcp as a network Device source.

Workspaces, templates, and presets

Choose what to save according to how much of the receiver you want to restore.

ToolSavesUse for
WorkspacePatch, rack, device references and settings, band-plan choiceA complete receiver layout
TemplateBuilt-in graph and radio configurationSetting up a common activity
PresetNamed snapshot of a workspace and bound device settingsRestoring a tuned setup
BookmarkFrequency and labelRetuning a selected device

Workspaces

Use the workspace name in the top bar to switch, create, or delete workspaces. The first workspace in a new database contains a Device, Scope, and Speaker. Later workspaces start empty.

Changes are saved on the server, including node positions, rack layout, and band-plan region. All connected clients share one active workspace.

Export and import

The ↓ button beside a workspace downloads a JSON file containing its name, patch, rack, band-plan choice, and node settings. The file excludes the database ID, revision, and undo history.

Import a workspace file creates and activates a new workspace. It never overwrites an existing one; duplicate names receive a copy number. Available radios are opened with the imported settings. Missing radios appear in the apply report, and their Device nodes wait for them. Select a replacement radio to use different hardware.

Files from a newer, unsupported format version are rejected.

Undo and redo

Use the top-bar arrows, Ctrl/⌘ Z, or Ctrl/⌘ Shift Z. Each workspace stores its last 100 layouts on the server, so undo and redo affect every connected client. Editing after undo discards the redo history.

Undo also updates the running graph. Undoing an added channel closes it; redo recreates it with its saved settings. Radio tuning is excluded from this history.

Copy and paste

Ctrl/⌘ C copies selected nodes and the wires between them. Ctrl/⌘ V pastes and selects the copies beside the originals. Repeated pastes are offset so they remain separate.

Connections to nodes outside the selection are excluded. A copied Device has no radio assigned; select one before using it. The clipboard lasts for the browser tab’s lifetime and works across workspaces.

Templates

Select a Device, then open Library → Templates. Applying a template immediately retunes that radio, sets its sample rate, and merges channels and compatible displays or outputs into the workspace. The apply button identifies the target radio.

Templates cover broadcast FM, airband, ADS-B, ACARS, AIS, APRS, paging, NAVTEX, radio clocks, GNSS, marine VHF, PMR446, digital voice, ISM, DAB, and common amateur bands. Templates that the selected radio cannot tune or sample are disabled.

Undo removes the added nodes, but does not restore the radio’s previous tuning or sample rate.

Presets

Save a preset after arranging and tuning a workspace. Applying it restores the graph and device settings using durable radio identities where available. The apply report lists anything that could not be restored.

Presets are editable and stored in the server database. Templates are read-only and ship with the app.

Bookmarks and band plans

A bookmark saves a frequency and label. Applying it tunes the selected Device and keeps the graph.

Bands searches the active regional allocation data. Choose the region from the workspace menu and enable the allocation ruler on Scope nodes if needed. Hover over the ruler for allocation details; click to tune, using the band’s usual mode when the data includes one.

Automatic region detection uses browser location and requires HTTPS or localhost. You can always choose the region manually.

Keyboard controls

Keyboard commands act on the selected node or on the device connected to the selected channel. They are ignored while a text field or control has focus. Press ? at any time to open the same reference inside the app.

KeysAction
Left / RightTune down or up one step
Shift + Left / RightTune ten steps
[ / ]Choose a smaller or larger tuning step
fFocus the device dial; press Enter to type a frequency
, / .Select the previous or next channel
m / MCycle the selected channel’s analog mode forward or backward
- / + or =Lower or raise squelch by 2 dB
sToggle squelch
19Select the nth node in the patch
pPin or unpin the selected node on the rack
vSwitch between Patch and Rack
Ctrl/⌘ ZUndo the last workspace change, for every connected client
Ctrl/⌘ Shift Z or Ctrl/⌘ YRedo
Ctrl/⌘ CCopy the selected nodes and the wires between them
Ctrl/⌘ VPaste them beside the originals
BackspaceDelete the selected node or wire
?Open the keyboard reference
EscapeClose an overlay or menu

Dial controls

Once the frequency dial has focus:

  • Left and Right choose a digit.
  • Up and Down change the selected digit.
  • Page Up and Page Down change the next larger place.
  • Home and End jump to the first or last place.
  • Enter opens direct frequency entry.

Click a Device node before using the mouse wheel on its dial. This prevents a wheel gesture from retuning the receiver when you intended to pan the canvas.

Troubleshooting

Start with the built-in diagnostic report:

sdrmm --doctor

In the interface, the same checks are under Check hardware on the Radios tab of an unbound Device node. Diagnostics identify backend, module, permission, device-discovery, database, and recording path problems before the engine claims any hardware.

The page does not open

  • Confirm the server logged sdr-- ready and note the address it printed.

  • On the same machine, try http://127.0.0.1:8080.

  • For another machine, confirm the server is bound to a reachable address, not loopback:

    sdrmm --bind 0.0.0.0:8080
    
  • Check the host firewall and container port mapping.

  • If a reverse proxy serves sdr– below a path prefix, reconfigure it to use a dedicated origin; the embedded application and API expect root-relative paths.

A token is rejected

The UI stores the shared token in browser local storage for that origin. If the server token changes, the next unauthorized response clears the old value and prompts again.

API clients should send Authorization: Bearer <token>. WebSocket and browser-initiated download URLs may send the same value as the token query parameter.

A radio is missing

  1. Confirm it appears in the operating system.
  2. Run sdrmm --doctor.
  3. For a SoapySDR receiver using the host runtime, run SoapySDRUtil --find.
  4. For SoapySDR modules, confirm ABI 0.8 and a directory in the reported search path.
  5. On Linux, check the USB node permissions and reconnect after installing udev rules.
  6. Stop other SDR programs; most devices can be claimed by only one process.

Desktop installers and containers use their bundled SoapySDR modules. To load an additional host module, set SDRMM_SOAPY_MODULE_PATH to its directory. Explicit paths are searched before the bundled directory. sdrmm --doctor prints the active search path.

An SDRplay receiver does not appear

Install the vendor API from SDRplay and confirm sdrplay_apiService is running. In sdrmm --doctor, SDRplay API reports the loaded library or the reason loading failed.

An RSPduo in use by another application lists only its available modes, possibly Slave alone. See SDRplay. Container access also requires the host library and shared IPC namespace; see SDRplay receivers.

A device is present but a saved node is disconnected

The node intentionally waits for the same durable device identity it stored earlier. This avoids binding your settings to a different receiver after enumeration order changes. Check the serial or variant shown on the node. Use Forget this radio only when you want to choose a replacement.

Spectrum works but audio is silent

  • Confirm the channel’s audio output is wired to a Speaker.
  • Start the stream from the Speaker node’s control.
  • Click once in the page if the browser blocked autoplay.
  • Disable squelch temporarily or lower its threshold.
  • Make sure the channel marker is over the signal and its full occupied bandwidth fits inside the device passband.
  • Check that the tab is not muted and that the system output device is correct.

A decoder produces nothing

  • Verify the exact frequency, channel variant, and expected baud or protocol setting.
  • Use a Scope to confirm energy is present and centered.
  • Check the sample-rate warning on the channel face. ADS-B requires a native device rate between 2 and 4 MS/s.
  • Wire the events output to the correct destination. Independent frames appear in Decoder log; accumulated targets appear in Readout; positions require a Map.
  • Increase gain cautiously and watch for clipping or Device overruns.

Overruns or gaps

An overrun means the capture thread produced samples faster than the DSP path could consume them. Those samples are lost, so spectrum, audio, recordings, and decoders can all contain gaps.

  • Lower the device sample rate.
  • Close unused channels, scopes, or decoders.
  • Avoid debug builds for real-time operation; the workspace optimizes DSP crates even in the dev profile, but custom profiles may not.
  • Check CPU frequency scaling and thermal throttling on small computers.
  • Prefer a wired network for high-rate remote receivers.

Recordings do not appear

  • Confirm --recordings-dir is writable by the server user.
  • With Docker, persist and inspect /data/recordings.
  • Stop an active recording so its metadata can be finalized.
  • A pair with invalid or missing SigMF metadata is not listed as playable.

Development server requests fail

Use cargo xtask dev, which starts the backend with development CORS and configures Vite to proxy the API and WebSocket. If starting the pieces manually, pass --dev-cors only for a trusted local development origin, since it permits cross-origin requests broadly.

Configuration and security

The sdrmm binary runs the receiver engine, REST API, WebSocket and MCP endpoints, Swagger UI, and embedded React application in one process.

Command-line options

sdrmm [OPTIONS]
OptionDefaultPurpose
--bind <ADDRESS>0.0.0.0:8080Address and port for HTTP and WebSocket traffic
--db <PATH>Platform data directorySQLite database for workspaces, presets, bookmarks, recording index, and decoder log
--recordings-dir <PATH>Platform data directoryDirectory containing SigMF recording pairs
--token <TOKEN>NoneRequire one shared bearer token for API, WebSocket, and MCP requests
--routing-backend <NAME>open-route-serviceRouting service: open-route-service or graph-hopper
--routing-url <URL>The backend’s own serviceBase URL, for a self-hosted instance
--routing-key <KEY>NoneAPI key for that service
--dev-corsOffAllow a separate frontend development origin
--doctorOffPrint environment diagnostics and exit
--helpShow CLI help
--versionShow the build version

Relative database and recording paths are resolved at startup. Use absolute paths for services and containers so storage does not depend on the working directory.

Persistent data

The SQLite database contains configuration and structured history. The recordings directory contains large IQ files. Back up both when you need a complete installation:

/srv/sdrmm/
├── sdrmm.db
└── recordings/
    ├── <capture>.sigmf-meta
    └── <capture>.sigmf-data

Stop the server or use SQLite’s supported backup mechanism before copying a live database. Raw recording pairs can be copied while idle; do not assume an actively written pair is complete.

Logging

sdr– uses the standard RUST_LOG filter. Without an override it logs general information and more detailed sdr– messages. Examples:

RUST_LOG=info sdrmm
RUST_LOG=sdrmm=trace,info sdrmm

Trace logging can be noisy on an active receiver. Capture it for a short diagnostic session rather than leaving it enabled on an unattended server.

Shared-token authentication

By default, a headless server is unauthenticated and trusts its local network. Set a long random token whenever untrusted clients can reach the port:

export SDRMM_TOKEN='replace-with-a-long-random-secret'
sdrmm

The environment variable avoids exposing the secret in the process list. --token and SDRMM_TOKEN configure the same value.

The browser prompts for the token and stores it in local storage for that origin. REST and MCP clients should send:

Authorization: Bearer replace-with-a-long-random-secret

WebSocket handshakes and browser download links can use ?token=... because those requests cannot always attach an authorization header.

The application shell and GET /api/auth remain reachable without authentication so the browser can load and discover that it needs a token. Other API, WebSocket, documentation, and MCP routes are protected.

Network security

The shared token is access control, not transport encryption. A plain HTTP client on the network can expose it and receiver traffic to an observer. For access beyond a trusted LAN:

  • bind to loopback and place an HTTPS reverse proxy or authenticated tunnel in front;
  • preserve WebSocket upgrade headers for /api/ws;
  • proxy the application at the origin root rather than a path prefix;
  • keep the direct 8080 port firewalled;
  • rotate the shared token if it may have leaked.

sdr– has one shared privilege level. It does not currently provide per-user accounts or read-only roles, and every authenticated client can change the active receiver.

Turn-by-turn routing

Field mode can request driving routes to direction-finding waypoints. The server proxies requests to OpenRouteService or GraphHopper and sends the API key in an Authorization header. The key is not sent to the browser or included in URLs.

Set --routing-key for the hosted backend. Use --routing-backend to choose the service and --routing-url for a self-hosted instance.

Without a configured or reachable backend, field mode reports that routing is unavailable and uses heading guidance. The phone’s navigation app remains available through Navigate in Maps.

Development CORS

--dev-cors installs a permissive CORS policy for the separate Vite origin used during frontend development. It is not needed when the UI is served by sdrmm, and should not be enabled as a production cross-origin access policy.

Containers and remote radios

A remote deployment keeps USB cable length short and moves control, decoded data, compressed audio, and display frames across the network instead of raw device IQ.

Docker Compose

The repository includes a single-service Compose configuration:

git clone https://github.com/Newspicel/sdrminusminus.git
cd sdrminusminus
docker compose pull
docker compose up -d

Open http://<host>:8080. The service stores its database and recordings in the named sdrmm-data volume and restarts unless stopped.

Use the nightly image only when you intend to track unreleased changes:

services:
  sdrmm:
    image: ghcr.io/newspicel/sdrminusminus:nightly

USB devices

The supplied Compose file passes the complete Linux USB bus:

devices:
  - /dev/bus/usb:/dev/bus/usb
device_cgroup_rules:
  - "c 189:* rmw"

The rule matters after a reconnect: a USB device may return with a different minor number than the one present when the container started. Host udev permissions still apply to the nodes. Prefer installing the receiver’s normal udev rule; when that is not possible, add the numeric group that owns the device with group_add. Running the whole service as root should be a last resort.

SDRplay receivers

The image carries the driver but not SDRplay’s vendor API, which is licensed for use with genuine SDRplay hardware and cannot be redistributed. Install the API on the host, leave its service running there, and give the container the library plus the shared memory the service talks over:

volumes:
  - /usr/local/lib/libsdrplay_api.so.3:/usr/local/lib/libsdrplay_api.so.3:ro
ipc: host

ipc: host lets the library communicate with sdrplay_apiService through POSIX shared memory. Without it, the API cannot open even when its library is mounted. This also exposes the host’s other IPC objects to the container, so use it only with a trusted image and host, outside multi-tenant deployments. If the API is missing, no RSP appears; other drivers still work.

Data and authentication

The image entry point fixes persistent paths under /data. Add a token through an environment file rather than committing it to Compose:

services:
  sdrmm:
    env_file: .env
SDRMM_TOKEN=replace-with-a-long-random-secret

Protect the environment file and back up the sdrmm-data volume. See Configuration and security before exposing the service outside a trusted LAN.

Run the portable server as a service

For a non-container deployment, give sdrmm a dedicated unprivileged account, explicit data paths, and a service manager that sends a normal termination signal. A representative command is:

/usr/local/bin/sdrmm \
  --bind 0.0.0.0:8080 \
  --db /var/lib/sdrmm/sdrmm.db \
  --recordings-dir /var/lib/sdrmm/recordings

Grant that account access through the radio’s udev rules. Graceful termination is important because the engine finalizes active recordings during shutdown.

Connect to a network receiver

sdr– can operate radios that already expose IQ over the network. Add a Device node, open the Network tab, then select:

  • rtl_tcp, default port 1234;
  • SpyServer, default port 5555.

Enter a DNS name, IPv4 address, or bracketed IPv6 endpoint. You can omit the port when using the default. These connections are named rather than discovered, and the saved workspace keeps the canonical endpoint as the device identity.

Network IQ can require substantial and sustained bandwidth. Use wired Ethernet where possible, select only the sample rate the task needs, and watch Device overruns and reconnect messages.

SoapyRemote

The desktop and container distributions also bundle SoapyRemote. Run SoapySDRServer beside the hardware, then choose the discovered remote device through the normal Device list. SoapyRemote is part of SoapySDR and is distinct from sdr–’s direct rtl_tcp and SpyServer backends.

Browser deployment

Modern browser audio works on localhost and ordinary LAN origins, but some features have secure context requirements. In particular, automatic band-region detection uses browser geolocation and normally requires HTTPS. Manual region selection does not.

When proxying through HTTPS, forward normal HTTP routes and WebSocket upgrades on the same origin. The UI uses /api/*, /api/ws, and /mcp root-relative paths.

API and automation

REST, WebSocket, and MCP control the same receiver as the web interface. Shared types in crates/wire define the API contract and generate OpenAPI schemas and TypeScript declarations.

Interactive reference

On a running server:

  • Swagger UI: /api/docs
  • OpenAPI JSON: /api/openapi.json
  • WebSocket: /api/ws
  • MCP streamable HTTP: /mcp

The repository also commits the generated openapi.json so clients can be generated without a running receiver.

When authentication is enabled, Swagger, REST, WebSocket, and MCP require the shared token. See Configuration and security.

REST resources

The API covers:

AreaExample routes
Discovery and state/api/devices, /api/channeltypes, /api/state, /api/clients
Live receiver/api/devicesets, device settings, channels, scanner, recording, playback
Workspaces/api/workspaces, activate, apply, undo and redo, export and import
Reuse/api/templates, /api/presets, /api/bookmarks
Data/api/decoderlog, exports, /api/recordings, downloads
Reference/api/bandplan/regions, /api/about, /api/doctor

Use Swagger for exact request bodies, status codes, and schemas. Errors use a consistent JSON body with error and optional detail fields instead of framework-specific plain text.

For an authenticated request:

curl \
  -H "Authorization: Bearer $SDRMM_TOKEN" \
  http://receiver.local:8080/api/state

WebSocket events and streams

The WebSocket carries control commands, state invalidations, decoder events, scanner progress, and binary spectrum, audio, and video frames. Stream-start events allocate identifiers per connection, so clients should not assume that another connection uses the same stream ID.

Use the generated schema and existing web client as the protocol reference. REST remains the authoritative way to fetch current durable state after an invalidation; high-rate samples and events are streamed rather than stored in that state response.

MCP

The MCP endpoint exposes receiver tools suitable for an automation client or assistant. Current tools can:

  • get state and discover devices or channel types;
  • open, close, and tune devices;
  • add or remove channels;
  • start and stop scans;
  • start or stop recordings;
  • query decoded history;
  • capture a spectrum snapshot;
  • list available measurement tools;
  • calculate antenna dimensions for a frequency;
  • discover, interrogate, sweep, and calibrate a NanoVNA.

Configure an MCP client for streamable HTTP at http://<server>:8080/mcp and attach the same bearer authorization header when the server uses a token. MCP actions affect the live shared receiver just like changes made in the interface.

Generated-code workflow

After changing a REST type or route in crates/wire or crates/server, regenerate the checked-in contract and TypeScript declarations:

cargo xtask codegen

This updates openapi.json and web/src/generated. cargo xtask check fails when either output has drifted from the Rust source.

Build and test

The workspace contains the headless server, desktop shell, frontend, DSP libraries, hardware backends, protocol decoders, and project tooling. CI calls the same cargo xtask commands used locally.

Prerequisites

  • Rust through rustup. The repository pins a nightly toolchain and the rustfmt, clippy, and rust-src components in rust-toolchain.toml.
  • Node 26.
  • pnpm 11; the exact package-manager version is declared in web/package.json.
  • SoapySDR 0.8 development files for the normal local-hardware build.
  • A C/C++ toolchain and CMake for native dependencies.

On Debian or Ubuntu:

sudo apt-get update
sudo apt-get install -y build-essential cmake libsoapysdr-dev

On macOS:

brew install cmake soapysdr

The first Cargo command automatically installs the pinned Rust toolchain. Do not substitute stable Rust: the workspace intentionally uses its pinned compiler and -Zpolonius=next configuration.

Build and run

git clone https://github.com/Newspicel/sdrminusminus.git
cd sdrminusminus
pnpm --dir web install --frozen-lockfile
pnpm --dir web build
cargo run -p sdrmm

The server embeds web/dist at compile time and listens on http://localhost:8080. Build the web application before compiling a distributable binary; when the directory is absent, the server build script creates a placeholder so backend-only development can still compile.

For local development:

cargo xtask dev

This runs sdrmm with development CORS on port 8080 and Vite with hot module replacement on http://localhost:5173. Vite proxies API and WebSocket traffic to the Rust server. Pass --watch to restart the Rust server whenever backend inputs change:

cargo xtask dev --watch

Backend feature flags

The default features are soapy, sdrplay, cr8, rtlsdr, hackrf, net-client, and gpu-fft. Disable defaults to build with virtual sources only:

cargo run -p sdrmm --no-default-features

To retain direct rtl_tcp and SpyServer support:

cargo run -p sdrmm --no-default-features --features net-client

The built-in virtual driver and recording playback are always available.

Local gates

CommandWhat it runs
cargo xtask checkToolchain checks, generated-data checks, Rust format and Clippy, frontend format/lint/type-check, release-shaped builds, web build, codegen drift
cargo xtask testRust and frontend unit/integration tests using virtual devices
cargo xtask smokePlaywright against a real sdrmm process and the virtual signal generator
cargo xtask auditcargo-deny and the RustSec advisory database
cargo xtask desktopTauri desktop compile gate without building installers

Install the smoke browser once before running the Playwright gate:

pnpm --dir web exec playwright install chromium
cargo xtask smoke

cargo xtask test requires cargo-nextest, and cargo xtask audit requires cargo-deny:

cargo install --locked cargo-nextest cargo-deny

Tests never enumerate real hardware in CI. Engine and server tests construct a registry with the virtual backend, which keeps them deterministic and prevents test runs from claiming an attached radio.

Generated files

Run the matching task whenever its source changes:

Source changeCommandGenerated output
REST routes or wire typescargo xtask codegenopenapi.json, web/src/generated/schema.d.ts
Dependency lockfilescargo xtask licensesTHIRD_PARTY_NOTICES.md, embedded notices JSON
web/pnpm-lock.yamlcargo xtask nix-hashThe pnpm store hash in packaging/nix/package.nix
Decoder reference signalscargo xtask fixturesSigMF pairs under fixtures/
Band-plan source importscargo xtask bandplanEmbedded regional tables
assets/icon.svgcargo xtask iconsDesktop and web icon variants

Generated outputs are committed. cargo xtask check detects drift for the outputs that must match on every change.

cargo xtask nix-hash uses Nix on Linux and a nixos/nix container elsewhere to compute the pnpm store hash. cargo xtask check compares the lockfile digest recorded beside that hash; it does not rebuild the store. The Nix CI job verifies the store hash itself.

Desktop prerequisites

The Tauri app is outside the workspace’s default members because Linux builds need WebKit and desktop integration packages. Build it explicitly through cargo xtask desktop. To create local installers, install the Tauri CLI and the platform prerequisites, then follow Release process.

Before opening a pull request

Run the checks proportional to the change. For documentation changes, build the book with mdbook build docs and validate local links and heading anchors. Code changes should normally run:

cargo xtask check
cargo xtask test

Add cargo xtask smoke, cargo xtask desktop, or a hardware validation when the affected surface requires it.

Architecture

The headless binary and desktop app use the same Rust server library and receiver engine. The browser and desktop window run the same React client.

                         control and state
┌──────────────┐       REST / WebSocket / MCP       ┌──────────────┐
│ React client │ ◀────────────────────────────────▶ │ Rust server  │
└──────────────┘                                    └──────┬───────┘
                                                           │ commands
                                                           ▼
┌──────────────┐   IQ blocks   ┌──────────────┐   ┌────────────────┐
│ SDR / file / │ ────────────▶ │ DSP engine   │ ─▶│ audio, events, │
│ network      │               │ and channels │   │ spectrum, IQ   │
└──────────────┘               └──────────────┘   └────────────────┘

Crate boundaries

CrateResponsibility
sdrmm-dspAllocation-free signal-processing primitives; no I/O or internal project dependencies
sdrmm-modemReusable modem algorithms depending only on DSP
sdrmm-modem-test-supportModem measurement catalogs, simulations, and baseline tooling; tests and developer tools only
sdrmm-wireShared settings, DTOs, events, patch graph, and OpenAPI schemas
sdrmm-deviceHardware-independent device traits, capabilities, settings, and registry
sdrmm-device-virtualSignal generators and SigMF playback
sdrmm-device-rtlsdrNative RTL-SDR driver
sdrmm-device-hackrfNative HackRF driver
sdrmm-device-soapyLocal hardware through SoapySDR
sdrmm-device-sdrplaySDRplay RSP receivers through the vendor API, loaded at runtime
sdrmm-device-netDirect rtl_tcp and SpyServer clients
sdrmm-device-cr8Dragon Labs CR-8 through the vendor SDK, loaded at runtime
sdrmm-device-arrayAlready-open streams composed as logical lanes; no hardware opens
sdrmm-channelsAnalog demodulators, protocol decoders, and their descriptors
sdrmm-recorderSigMF writing, reading, scanning, and export
sdrmm-engineDevice supervision, channelization, scanning, streams, recording, and state snapshots
sdrmm-serverREST, WebSocket, MCP, persistence, band plans, auth, and embedded assets

apps/sdrmm handles CLI configuration and process lifetime. apps/desktop binds the server to an ephemeral loopback port and points a Tauri WebView at it. With SoapySDR enabled, both call sdrmm_device_soapy::enable_isolated_probes during startup. Discovery re-executes the binary as a short-lived probe helper, isolating the application from crashes in vendor discovery code.

One source of truth for wire types

REST bodies, WebSocket messages, settings, and patch types are declared in crates/wire. utoipa derives the OpenAPI schemas, and cargo xtask codegen generates the TypeScript client types. A new field should not be re-declared independently in Rust, OpenAPI, and TypeScript.

The frontend asks the server for device capabilities, channel descriptors, and the node palette. This keeps UI controls and connection rules aligned with the running build.

Data plane and control plane

The real-time DSP path does not perform HTTP, database I/O, asynchronous work, or UI formatting. It receives settings through command queues and publishes bounded snapshots and output buffers. Hot processing avoids locks and allocation so timing is predictable under a continuous sample stream.

The control plane can block or allocate where appropriate. It owns Axum handlers, SQLite, workspace reconciliation, recording indexes, client subscriptions, and protocol serialization.

High-rate data uses binary WebSocket frames. Durable state stays behind REST, and WebSocket state events tell clients which query scope to refetch. Decoder events are typed JSON because their rate and structure suit it; audio is Opus-compressed before crossing to the browser.

Coherent processing

An ordinary channel reads one lane. A coherent processor needs aligned samples from every lane of one source.

Each capture block carries its first sample index. Drivers advance the index over hardware-reported losses, preserving gaps. While coherent processing is active, each lane also writes to a tap ring that records any samples it cannot retain.

The aggregator finds the largest sample range common to all lanes. After a gap, it discards samples up to the next common index, then applies calibrated delays and complex weights. Like dsp_loop, it runs without locks, allocation, or async work.

Processors can return beamforming weights. The aggregator sums the weighted lanes into an ordinary capture ring after the source’s physical lanes. Channels, recorders, and spectrum subscriptions consume this beam through the normal single-lane path.

A patch Array node composes streams from existing Device nodes. sdrmm-device-array supplies logical ingress lanes; the engine forwards the members’ corrected IQ through bounded rings and coordinates tuning. Device nodes retain ownership of their radios and channels. The engine handles membership changes and member recovery without opening hardware through the array adapter.

Channel media, spectrum, recording blocks, and coherent results cross preallocated SPSC buffer pools before workers allocate transport payloads or call broadcast senders. Saturation never waits on the DSP thread: media loss is reported, and recordings fail explicitly. Worker shutdown drains pending buffers. Decoder algorithms may still allocate their own variable-sized results.

channels depends on dsp, modem, and wire; protocol-independent modem algorithms stay in modem. sdrmm-test-support contains allocation and throughput measurement helpers. modem-test-support owns the modem measurement harness and JSON baseline tooling. Neither appears in the application’s normal dependency graph. cargo xtask check enforces these boundaries; cargo xtask perf runs the DSP allocation and throughput gates, full-slot decoder search baselines, and engine publication checks.

Workspaces and live engine state

A workspace graph is desired state. Applying it binds durable Device nodes to currently discovered devices, opens or closes engine objects, restores device settings, and creates the channels implied by IQ connections.

The graph never stores engine IDs because those are allocated anew on each run. Device references use backend, serial, key, and variant identity; channel binding follows graph order and source ports. An unplugged receiver therefore leaves a meaningful disconnected graph instead of corrupting the saved workspace.

Failure and backpressure

The project favors bounded queues and explicit loss reporting over unbounded memory growth. Device overruns, dropped decoder frames, recording faults, truncated exports, WebSocket lag, and reconnect state surface to clients. A busy consumer should not be able to stall the capture thread or grow the process indefinitely.

Testing layers

  • DSP primitives use analytic and golden-vector tests.
  • Decoders use synthesized or recorded IQ fixtures with expected typed output.
  • Engine tests run end-to-end through virtual devices.
  • Server tests exercise handlers, persistence, WebSocket behavior, auth, and OpenAPI shape.
  • Frontend tests cover pure view logic and stores; Playwright runs a complete browser flow.
  • CI builds the Soapy release shape without enumerating host modules or requiring hardware.

Keep tests at the narrowest layer that proves a behavior, then add an end-to-end fixture when a decoder or cross-layer workflow needs it.

Standard tables and their provenance

Some decoder constants come directly from specifications:

ConstantsLocation
DAB puncturing and protection profilescrates/channels/src/dab/protection.rs
DAB phase referencecrates/channels/src/dab/ofdm.rs
DVB-S puncturing and Reed–Solomon parameterscrates/channels/src/datv/dvbs.rs
DVB-S2 LDPC accumulator addressescrates/channels/src/datv/dvbs2/tables/
VL-SNR header sequencecrates/channels/src/datv/dvbs2/vlsnr.rs

Sources are ETSI EN 300 401 (DAB), TS 102 563 (DAB+), EN 300 421 (DVB-S), EN 302 307-1 and -2 (DVB-S2/S2X), TS 102 606 (GSE), and ES 201 980 (DRM).

Table values were cross-checked against welle.io (GPL-2.0-or-later) and GNU Radio’s gr-dtv (GPL-3.0-or-later). This attribution concerns table verification, not copied decoder code. The 7,378 DVB-S2 accumulator addresses were transformed mechanically. The VL-SNR 896-bit seed and Walsh–Hadamard rows were transcribed from the standard; their sixteen generated patterns match gr-dtv’s tables.

Tests check independent properties such as puncturing density, polynomial roots, published CRC values, and parity checks on encoded words. These checks help detect transcription errors.

Release process

Releases are tag-driven and build portable server archives, desktop installers, update bundles, and multi-architecture container images. A scheduled workflow publishes a rolling nightly only when main has changed since the previous nightly.

Versioning

The root [workspace.package] version is the source of truth. The desktop configuration inherits it, and the release workflow stamps it from the tag with:

cargo xtask set-version 1.2.3

Release versions must be plain major.minor.patch. The major and minor components must fit in eight bits and patch in sixteen bits because Windows MSI ProductVersion cannot represent larger values or prerelease suffixes. The task rejects invalid versions before a bundle job starts.

Stable release tags use v<major>.<minor>.<patch>. Nightlies use the UTC date as YY.M.D, which also remains within the MSI limits.

Portable archives

Build the same archive produced in CI:

cargo xtask dist
cargo xtask dist --target aarch64-unknown-linux-gnu

The command installs a missing Rust target, builds the frontend, compiles the release binary with Soapy and network backends, verifies the embedded UI, and writes a .tar.gz or .zip under dist/ with README and license files.

Portable archives link against SoapySDR but do not bundle its runtime. Test the archive on a clean machine with the documented SoapySDR 0.8 dependency.

Desktop bundles

Without --bundles, the desktop task is the compile gate used on pull requests:

cargo xtask desktop

Creating installers requires the Tauri CLI:

cargo install --locked tauri-cli
cargo xtask desktop --bundles dmg

Use deb,appimage on Linux and msi,nsis on Windows. Before bundling, stage the matching locked Soapy runtime into apps/desktop/resources/soapy with the scripts under packaging/soapy, then verify the result:

cargo xtask soapy-bundle-check

Release CI performs this staging from the immutable platform lockfiles. A bundle must include the core, baseline modules, transitive libraries, and their notices.

Desktop updates

The desktop app checks the newest non-prerelease GitHub release once at startup. Update archives are signed separately from platform code signing with the Tauri updater key. The public key is compiled into the application; losing the private key prevents updates to already installed clients.

When a local signing key is absent, the bundle task passes --no-sign and produces installers that cannot be published as application updates. Release CI requires signatures and creates latest.json from them:

cargo xtask updater-manifest \
  --version 1.2.3 \
  --dir dist/release \
  --base-url https://github.com/Newspicel/sdrminusminus/releases/download/v1.2.3

Containers

The release workflow builds Linux amd64 and arm64 images and publishes a manifest at:

ghcr.io/newspicel/sdrminusminus:<version>
ghcr.io/newspicel/sdrminusminus:latest

Nightlies update only the nightly tag. Image smoke tests run the binary, inspect Soapy modules, start the server, and verify that it serves the built frontend.

Homebrew tap

Newspicel/homebrew-tap carries a sdrmm formula for the portable server and a sdrminusminus cask for the desktop application. Both describe published downloads rather than a source build, so the release workflow writes them after the release exists:

cargo xtask homebrew-tap \
  --version 0.4.0 \
  --sums SHA256SUMS \
  --repo Newspicel/sdrminusminus \
  --out ../homebrew-tap

The generator reads digests from the release’s SHA256SUMS and fails if a required artifact is missing. Updating the tap requires HOMEBREW_TAP_TOKEN with write access. Without that secret, the tap job is skipped while the release continues.

The tap publishes stable releases only. Validate generator changes with Homebrew:

brew style newspicel/tap
brew audit --strict --online newspicel/tap/sdrmm
brew audit --strict --online --cask newspicel/tap/sdrminusminus

Release checklist

Before tagging:

  1. Run cargo xtask check, cargo xtask test, cargo xtask smoke, and cargo xtask audit.
  2. Run cargo xtask desktop and build the container.
  3. Confirm generated API, license, fixture, icon, and band-plan outputs are current.
  4. Validate supported hardware with the candidate package, including a reconnect and recording.
  5. Confirm the updater signing secret and platform signing credentials are available.
  6. Tag the exact reviewed commit and watch every artifact matrix job.
  7. Install or unpack at least one published artifact and run sdrmm --version and sdrmm --doctor.

Use the release workflow’s manual dispatch as a rehearsal. It builds and uploads the full artifact matrix without publishing a GitHub release.