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

Architecture

The desktop app and the headless server run the same Rust server and receiver engine, and serve the same React interface.

React client ↔ REST / WebSocket / MCP ↔ Server control plane
                                              ↓ commands
Radio / network / recording → DSP engine → audio, events, spectrum, IQ

Crates

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-recordingSigMF playback behind the Recording node
sdrmm-device-siggenSignals for the Signal generator node
sdrmm-device-virtualSynthetic radios for debug builds and tests
sdrmm-usb-streamBulk USB streaming shared by the native drivers
sdrmm-device-rtlsdrNative RTL-SDR driver
sdrmm-device-airspy, sdrmm-device-airspyhfNative Airspy drivers
sdrmm-device-hackrfNative HackRF driver
sdrmm-device-ad936xAntSDR, PlutoSDR and other AD936x boards, speaking iiod over Ethernet or USB
sdrmm-device-soapyLocal hardware through SoapySDR
sdrmm-device-sdrplaySDRplay RSP receivers through the vendor API, loaded at runtime
sdrmm-device-rtltcpDirect rtl_tcp client
sdrmm-device-spyserverDirect SpyServer client
sdrmm-device-sdrconnectSDRplay SDRconnect over its WebSocket API
sdrmm-device-kiwisdrKiwiSDR over its WebSocket API
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, their descriptors, and signal synthesis
sdrmm-recorderSigMF writing, reading, scanning, and export
sdrmm-orbitSGP4, pass prediction, and Doppler
sdrmm-toolsAntenna calculator and NanoVNA
sdrmm-cpsCodeplug reading, writing, and conversion
sdrmm-test-supportAllocation and timing helpers for tests
sdrmm-engineDevice supervision, channelization, scanning, streams, recording, and state snapshots
sdrmm-serverREST, WebSocket, MCP, persistence, band plans, auth, and embedded assets

apps/sdrmm is the CLI and owns the process. apps/desktop starts the same server on a random loopback port and opens it in a Tauri window. Both probe SoapySDR in a short-lived child process.

The dependency rules:

  • dsp does no I/O and depends on no project crate.
  • modem builds reusable modulation algorithms on dsp only.
  • channels depends on dsp, modem, and wire.
  • Measurement tooling lives in test-support crates, outside the application graph.

cargo xtask check enforces them.

One source of truth for wire types

REST bodies, WebSocket messages, settings, and the patch graph are defined once in crates/wire. OpenAPI derives from them, and cargo xtask codegen generates the TypeScript types.

The client builds its controls from what the server reports: device capabilities, channel descriptors, and the node palette. A control never exists in the UI that the running build does not support.

Control plane and DSP plane

The DSP path takes settings through command queues and publishes through bounded snapshots and buffers. It never does I/O, takes a lock, allocates, or awaits.

The control plane owns HTTP, SQLite, workspace reconciliation, subscriptions, and serialization. It may block and allocate.

Media and recording data leave DSP through preallocated single-producer, single-consumer buffer pools. Workers turn them into network payloads. A full queue never blocks DSP: lost media is reported and recordings fail loudly. Some decoders still allocate for variable-size results.

Spectrum, audio, and video travel as binary WebSocket frames; browser audio is Opus. Decoder events are typed JSON. After a WebSocket invalidation, clients fetch durable state over REST.

cargo xtask perf measures DSP throughput, allocation, decoder searches, and publication.

Coherent processing

Every capture block carries the index of its first sample, so reported hardware gaps are visible. Coherent processing buffers each lane and works on the sample range all lanes share. After a gap it skips to the next shared index, then applies the calibrated delays and weights.

A beam is written to an ordinary capture ring, so channels, recorders, and scopes use it like any single-lane source.

An Array node combines streams that Device nodes already own. device-array exposes them as logical lanes. The engine forwards corrected IQ, coordinates tuning, and recovers members. The array never opens hardware itself.

Workspaces and the live engine

The workspace graph is the desired state. Applying it binds saved Device references to found radios, restores their settings, and reconciles channels and engine objects.

Saved references identify a radio by backend, serial, key, and variant. Engine IDs are temporary and never saved. A disconnected radio keeps its node and settings until it returns.

Placing channels on radios

When Devices tune themselves, the control plane searches for tuning windows that cover the most channels, using branch-and-bound. Each independently tunable stream gets one window. The search respects wires, bandwidths, tuning ranges, manual settings, and pinned channels.

It stops after 50 ms or 100,000 search nodes and keeps the best answer found. Apply reports include placement.heard and placement.upper_bound. When they are equal, coverage is proven optimal for that snapshot. Ties favour existing placements.

Tests compare the search with an exhaustive oracle. For the larger comparison:

cargo test -p sdrmm-engine --lib compares_realistic_sizes -- --ignored --nocapture

Failure and backpressure

Every queue is bounded. Drops, recording faults, truncated exports, WebSocket lag, and reconnects are reported to clients. A slow consumer can never block capture or grow memory without limit.

Tests

LayerTested with
DSPAnalytic and golden vectors, allocation and throughput gates
DecodersRecorded IQ with expected output, generated vectors
EngineEnd-to-end runs on virtual devices
ServerHandlers, persistence, streams, auth, OpenAPI, codegen drift
ClientUnit tests and browser smoke flows

Test at the narrowest layer that proves the behaviour. Add end-to-end coverage when a change crosses layers. CI never touches real radios.

Tables from standards

Some decoder constants are copied from the standards:

ConstantsFile
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/
DVB-S2X LDPC addresses, constellations and interleaverscrates/channels/src/datv/dvbs2/s2x/
VL-SNR header sequencecrates/channels/src/datv/dvbs2/vlsnr.rs
DVB-T continual pilot and TPS carrierscrates/channels/src/datv/dvbt/en300744.rs
DVB-T2 pilots, reserved carriers, P1, L1 and LDPC tablescrates/channels/src/datv/dvbt/t2/en302755/

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

The DVB-S2/S2X tables are generated from and checked against the ETSI PDF text by s2x_tables.py and dvbs2_spec_check.py in crates/modem-test-support/scripts/. dvbt_tables.py generates the DVB-T and DVB-T2 tables the same way. CI runs all three. The VL-SNR seed and Walsh-Hadamard rows were typed from the standard.

Tests catch transcription errors by checking independent properties: puncturing density, polynomial roots, published CRC values, and parity of encoded words.