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 and waveform synthesis, 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 a Device node
sdrmm-device-virtualSynthetic radios, the bench scenes and the signal generator
sdrmm-usb-streamBulk USB streaming shared by the native drivers
sdrmm-serialSerial port enumeration, one filtering policy, and the serial library every crate uses
sdrmm-device-rtlsdrNative RTL-SDR driver
sdrmm-device-airspy, sdrmm-device-airspyhfNative Airspy drivers
sdrmm-device-hackrfNative HackRF driver
sdrmm-device-funcubeFUNcube Dongle Pro+, over USB audio and HID
sdrmm-device-espsdrESP32 running ESP-SDR firmware, over serial
sdrmm-device-ad936xAntSDR, PlutoSDR and other AD936x boards, speaking iiod over Ethernet or USB
sdrmm-iqlinkUDP sample protocol shared by sdrmm-iqlinkd and sdrmm-device-ad936x
sdrmm-iqlinkdDaemon on AD936x boards that streams samples over UDP
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-aaroniaAaronia SPECTRAN V6 and V6 ECO through the RTSA API, loaded at runtime
sdrmm-channelsEvery built-in decoder, each a module with its settings, events and registration, plus the decoder registry and signal synthesis
sdrmm-dvsiDVSI AMBE vocoder chips over USB serial
sdrmm-pluginsLoads WASM plugins into the registries
sdrmm-pluginSDK for writing a WASM plugin
sdrmm-plugin-examplePlugin template
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, phones, and embedded assets
sdrmm-mobile-corePairing, pinned TLS client, missions, and pose fusion for the phone apps

Nothing outside sdrmm-channels names a decoder. The engine, server and frontend read the decoder registry: descriptors, settings forms, event kinds with their presenters, feeds and trunking. A shared behavior is a capability or facet a decoder declares.

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 Chromium (CEF) window. Both probe SoapySDR in a short-lived child process. apps/ios and apps/android are native phone apps on sdrmm-mobile-core.

The dependency rules:

  • dsp does no I/O and depends on no project crate.
  • modem builds reusable modulation algorithms on dsp only.
  • channels pulls no internal crates beyond dsp, modem and wire.
  • Device crates never depend on channels, engine or server.
  • mobile-core depends on wire only. Phone builds pull no engine, server, DSP, USB, FFmpeg, codec2, or SQLite crate.
  • test-support and modem-test-support never reach the sdrmm dependency graph.

cargo xtask check enforces the dependency rules.

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 allocate for variable-size results.

Spectrum, audio, IQ and video travel as binary frames: audio is Opus, video is JPEG and IQ is 16-bit against each block's peak. Clients that ask for the same spectrum size and rate share one reduction, and clients watching one picture share one encoding. Decoder events are typed JSON. Over WebTransport, events keep their order on one stream, audio goes in datagrams and every other stream of frames has a QUIC stream to itself, so one lost packet delays one picture. The WebSocket carries the same messages in one ordered queue. After an invalidation, clients fetch durable state over REST.

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

Arrays

Every capture block carries the index and time of its first sample, so hardware gaps are visible. Each radio lane has a dormant tap. An Array node wakes the taps of its lanes; the engine's array runtime aligns them in time, solves delay, phase and gain on the noise source or a pilot, and corrects each lane with one filter. A lane that slips is realigned and reported.

Array processors run on the aligned block, inline, on worker threads, or dedicated on threads and backends the engine gives them, such as the radar's GPU. Each declares its execution, actions, GPU use and room for events in its descriptor. Their lane outputs, such as a beam or a stitched band, are virtual lanes of the first member radio, so channels, recorders and scopes use them like any radio lane.

The Array never opens hardware. Device nodes own the radios; the Array holds the tuning and gain of its lanes while it exists.

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).

s2x_tables.py and dvbs2_spec_check.py in crates/modem-test-support/scripts/ generate the DVB-S2/S2X tables from the ETSI PDF text and check them against it. dvbt_tables.py does the same for DVB-T and DVB-T2. The CI etsi job runs all three. The VL-SNR seed and Walsh-Hadamard rows are typed from the standard.

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