A cross-platform receiver app for RTL-SDR dongles (RTL2832U + R820T/R820T2, ~24–1766 MHz). It is RTL-SDR specific — other SDRs (Airspy, HackRF, SDRplay, …) are not supported. A small Python backend owns the dongle and does the signal processing; a web frontend (opened in a browser) shows the UI, waterfall, audio and maps. The two talk over a WebSocket.
📖 New here? See the User Guide — how to use every mode, plus things to try. Status:
| Mode | What it does | Status |
|---|---|---|
| Radio | Live scrolling waterfall + scope; click a signal to listen (WFM/NFM/AM/USB/LSB/CW), squelch, browser audio. Scroll to zoom. | ✅ working |
| Sweep | Swept wideband panorama (e.g. whole 88–108 band) to find signals | ✅ working |
| Scanner | Cycle a channel preset (Marine VHF / PMR446 / Airband) and stop on a transmission | ✅ working |
| Replay | Play a saved .cu8 capture back through the spectrum + demodulators — no dongle needed |
✅ working |
| DAB | DAB/DAB+ digital radio — ensemble station list + playback (via welle-cli) |
✅ working† |
| ADS-B | Aircraft on a Leaflet map (via dump1090) |
✅ working* |
| AIS | Ships on a Leaflet map (via AIS-catcher) |
✅ working** |
| APRS | Packet-radio stations on the map + live packet feed (via rtl_fm → direwolf) |
✅ working‡ |
| ACARS | Aircraft VHF data messages as a live feed (via acarsdec) |
✅ working§ |
| Satellite (beta) | Digital weather-satellite imagery (Meteor-M LRPT) via SatDump | 🧪 experimental |
| APT | NOAA weather-satellite images at 137 MHz (hand-written decoder) | ✅ working¶ |
| SSTV | Slow-scan TV images — auto-detects Martin / Scottie / Robot / PD (hand-written decoder) | ✅ working◇ |
| Pager | POCSAG/FLEX pager messages as a live feed (via multimon-ng) |
✅ working◆ |
| ISM | 315–915 MHz ISM-band devices — weather stations, TPMS, sensors, remotes (via rtl_433) |
✅ working‖ |
* Needs dump1090 installed (brew install dump1090-mutability) and a decent
1090 MHz antenna — the stock whip barely hears ADS-B. The pipeline runs and
plots aircraft when it receives them; with the stock antenna you may see none.
** Needs AIS-catcher (build from source, see below). AIS (162 MHz) works with a
normal VHF/whip antenna near water. By default we pass -X off so your received
data is NOT uploaded to the aiscatcher.org community feed.
Open the app and click Radio: you see the live waterfall + scope of the captured band, silent. Click any signal to listen — audio starts and the demod controls appear. Drag across a signal to set its bandwidth. A live spectrum scope sits above the waterfall (dBFS vs frequency) and a squelch slider mutes the audio when the channel level falls below the threshold (the level meter shows the current channel level and ▶/🔇). Adjust demod (FM/AM), bandwidth and volume in the Radio controls. The dongle stays on one center frequency and captures ~2.4 MHz; you're selecting channels within that band digitally — type a frequency in Center (Enter) to move the captured window.
Radio is one combined view: it's both "browse the band" and "listen." It stays silent until you click a signal, so it doubles as a plain waterfall.
Audio uses a ~200 ms jitter buffer to stay click-free, and trims any backlog left over after a network stall so latency stays bounded. The device is read on a dedicated thread (kept drained at real time) so DSP never starves the USB stream.
The dongle only sees ~2.4 MHz at once, so Sweep sweeps it across a wider range (set Sweep from/to in MHz, or pick a preset like FM 88–108 or Airband) and stitches the slices into one wide spectrum + waterfall. Click any peak to re-center the dongle there and drop straight into the Radio view to listen. Wider ranges sweep more slowly (each ~2.4 MHz slice needs its own retune + capture).
Select Scanner and pick a preset — Marine VHF (Ch 16 + ship-to-ship + Swedish leisure/fishing), PMR446, or Airband (AM). It cycles the channels, watching every channel in a 2.4 MHz block at once via one FFT, and parks on the first that breaks squelch, playing it until it's quiet for a few seconds, then resumes. Each channel shows a live signal bar so you can set squelch (dB over noise) by eye — lower it if wanted calls don't stop, raise it if it stops on noise. Set a Priority channel (e.g. Marine Ch 16) and the scanner pre-empts to it whenever it's active, even while parked elsewhere. Turn on voice squelch and it won't stop for channels carrying only data or a dead carrier — it listens for about 0.6 s, resumes scanning if there's no speech, and skips that channel for 15 s so the same pager doesn't catch it again next pass. Customize channels lets you edit/reorder/add channels (NFM or AM) and save your own presets (persisted on the backend; built-ins can't be overwritten). Search a range (beta) sweeps a whole frequency span instead of a channel list — enter from/to MHz, a step (5–100 kHz) and NFM/AM, and every step becomes a scanned slot; it parks on whatever breaks squelch (off-grid signals land on the nearest slot; very wide ranges are clamped to 800 slots). Marine VHF needs a VHF/marine antenna; Ch 16 is the easiest to test with.
Select Replay and click a saved .cu8 capture: it streams the file back
through the same Radio view and demodulators (looping at the end), so you can
re-open a capture and pull out any signal in the recorded band — no dongle
needed. Click a signal to listen, drag to set bandwidth, scroll to zoom, just
like live. The capture's center frequency and sample rate come from its filename.
- Scroll (mouse wheel) over the scope/waterfall to zoom the display into part of the captured band; shift-drag to pan; Zoom out (top-right) resets. This is a display zoom — it magnifies what's captured without retuning. (In Sweep, drag still narrows the swept range; plain drag in the Radio view sets demod bandwidth.)
- Gain — uncheck Auto gain for a manual slider over the device's gain steps. High manual gain helps weak signals (e.g. ADS-B); too much overloads.
- PPM correction — RTL-SDR crystals are off by tens of ppm; set this so the displayed/tuned frequency is accurate (important for narrowband + digital modes).
Radio supports WFM (broadcast), NFM (narrow — ham/marine/PMR voice), AM (carrier-normalised), USB/LSB (SSB, with AGC), and CW (Morse — plays the tone and decodes it to text in an overlay; best on clean signals, self-calibrates after a character or so). Switching demod sets a sensible default bandwidth you can then fine-tune. For WFM, FM stereo (off by default — enable it to decode the 38 kHz L−R subcarrier when a 19 kHz pilot is present, falling back to mono otherwise; a "◖◗ stereo" indicator shows when locked), an FM de-emphasis selector picks 50 µs (Europe) or 75 µs (Americas/Korea), and RDS decoding (off by default) shows the station name, radiotext, PI code and program type from the 57 kHz data subcarrier — hand-written decoder (pilot PLL → coherent BPSK → biphase / differential → block-syndrome sync → group parsing), no external tool.
Because the dongle captures a full ~2.4 MHz band, the Radio view can demodulate up to three extra channels (NFM/AM) in parallel with the main one and mix them into the audio — each with its own frequency, squelch and volume, shown as coloured cursors on the waterfall. A Click tunes A/B/C/D switch picks which receiver a waterfall click retunes. Works in Replay too. See the guide.
- Audio: the Record audio button (Radio controls) captures what you're hearing to a WAV download.
- IQ: Record IQ (Recording panel, Radio view) writes the raw stream to a
standard .cu8 file — replayable in Cascade's own Replay mode, or in
rtl_sdr/gqrx/etc. — listed with download/delete; the filename carries the center
frequency and sample rate.
- IQ is heavy (~290 MB/min at 2.4 MS/s). By default it goes to
backend/recordings; setCASCADE_RECORDINGS_DIRto write somewhere with room to spare and less wear — e.g. a USB drive, or an NFS-mounted NAS share (handy on a Pi to keep big captures off the SD card). On a Pi, prefer Ethernet while recording so a WiFi stall can't drop samples mid-capture. - The destination has to sustain 4.8 MB/s at 2.4 MS/s. If it can't, blocks are dropped and the capture has gaps in it while still looking like a good file — so stopping a recording reports what it lost (⚠ lost N blocks). Watch for that the first time you record to a new destination.
- IQ is heavy (~290 MB/min at 2.4 MS/s). By default it goes to
Under the band label, a live hint tells you how to set the RTL-SDR.com dipole
antenna kit for the entered/tuned frequency: whether to use the long (large)
or short (small) telescopic elements, the length to extend each to (≈ a
quarter wave, length_cm ≈ 7125 / f_MHz less the 2 cm internal), and the
orientation (vertical for terrestrial signals; a horizontal "V" for 137 MHz
weather satellites). It updates as you type a Center frequency.
- Band label: under the device status it names the service(s) on the current frequency range (FM broadcast, Airband, Marine VHF, 2 m/70 cm ham, TETRA, DAB, ADS-B, …) so you know what kind of traffic to expect (EU/Sweden band plan).
- Display panel: Auto contrast (or manual floor/ceiling dB) for the waterfall, Peak hold on the spectrum scope — peaks linger then fade over ~1–2 s so brief/bursty signals flash and are easy to spot — and Averaging (2–16×) to smooth the scope's noise floor so weak, steady carriers stand out.
- Frequency directory: a built-in, searchable, click-to-tune reference of known
channels (full ITU Marine VHF table, Stockholm airband, ham/APRS/sat
calling frequencies, weather-sat/ISM, PMR446). Click a row to jump there
in Radio mode with the right demod. The bundled lists are the author's: marine /
ham / weather-sat / ISM / PMR446 are standard Region-1 plans, but the airband
list is Stockholm-specific. Import your own
CSV/JSONlist (shown as My list), optionally untick Show built-in lists to use only yours, or editfrontend/src/frequencies.tsto change the defaults. - Bookmarks: save the current frequency (+ demod) with a name; click to recall, × to delete.
- HF modes (Reception ▸ Advanced): reach shortwave via an inline upconverter (Ham It Up; LO editable, default 125 MHz) or the V3's direct sampling input (~0.5–14.4 MHz, no extra hardware). Either way the whole app — tuning, waterfall axis, band labels, bookmarks, antenna helper — works in real HF frequencies (shortwave/AM broadcast, 80/40/20 m ham with the SSB/CW demods).
- Advanced reception knobs: tuner IF bandwidth (tame a strong neighbouring station), RTL AGC (the 2832's digital AGC stage), a tuning raster (5–100 kHz incl. 8.33 airband — wheel/arrows step by it, click-to-tune snaps to it), and DC-spike-free tuning (typed/recalled frequencies park the hardware centre off-channel automatically).
- Listening tools: CTCSS/DCS readout + tone squelch on NFM (see what sub-tone a repeater uses; open only for your tone), voice squelch on NFM/AM/SSB (stay muted unless the channel is carrying speech — cuts out pager bursts, data channels, stuck carriers and test tones, with a live score so you can set its sensitivity by eye), an impulse noise blanker, a manual notch filter, and an adjustable SSB passband + AGC speed for USB/LSB/CW.
- Settings persist across reloads (gain, PPM, bias-T, HF mode, raster, demod, volume, squelch, contrast, peak-hold, sweep range, receiver location, bookmarks) via localStorage.
The live controls (frequency, demod, volume, squelch, gain, level meter) sit in a control bar above the display, always visible; setup panels and lists live in the left sidebar. The spectrum scope + waterfall fill the rest of the window and resize with it.
Select ADS-B: the backend spawns dump1090, reads its aircraft.json snapshot
(once a second), and plots aircraft on an OpenStreetMap map. Switching to another mode
kills dump1090 and hands the dongle back. Bias-T (Reception ▸ Advanced) can power a
1090 MHz LNA — strongly recommended for real range. Gain/PPM are passed to dump1090.
Planes are drawn as icons that point their heading, sized by class
(narrowbody/small · widebody · A380), with a helicopter icon for rotorcraft. The
Aircraft list (sorted by distance from your location) shows a climb/descent
arrow by each altitude — ▲ climbing, ▼ descending, – level. Click a plane or a row
to follow it — the map re-centres and tracks it (drag to stop). The popup gives full
detail (callsign, ICAO, registration + model when your dump1090 build has an
aircraft DB, category — light/small/large/heavy/rotorcraft, squawk, altitude,
climb, speed, track). Each aircraft also draws a track trail as it moves.
Online lookup (opt-in): route, airline and tail number aren't in the ADS-B signal, so the "Look up route + tail number" toggle (off by default) fetches them from adsbdb.com — the route/airline by callsign and the registration (tail #) + operator by ICAO hex (the latter fills the tail number when your dump1090 build has no aircraft DB). The popup then shows airline, From → To, tail number and operator. Only well-formed airline callsigns are looked up for routes; results are cached and the route reflects the flight number's last-known route (so a stale/return leg is possible). Leaving the toggle off keeps ADS-B fully offline.
Select DAB, pick a Band III block (5A–13F). The backend runs welle-cli,
which tunes the block and decodes the ensemble; the station list appears on the
right — click a station to play it (the browser streams it from welle-cli). One
block carries many stations. The playing station shows its now-playing text
(DLS — song/programme info, DAB's RDS-radiotext equivalent), updating live.
Switching modes stops welle-cli and frees the dongle.
welle-cli isn't in Homebrew, so build it once:
brew install cmake fftw faad2 mpg123 libsamplerate lame
git clone --depth 1 https://github.com/AlbrechtL/welle.io.git ~/.local/src/welle.io
cd ~/.local/src/welle.io && mkdir build && cd build
export CPLUS_INCLUDE_PATH="$(xcrun --show-sdk-path)/usr/include/c++/v1" # macOS CLT libc++
cmake .. -DBUILD_WELLE_IO=OFF -DBUILD_WELLE_CLI=ON -DRTLSDR=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5
make -j4 welle-cli && cp welle-cli /opt/homebrew/bin/welle-cliNeeds a Band III antenna. Verified live in Stockholm: block 12C = the SR STOCKHOLM ensemble (P1–P4, etc.).
Select AIS: the backend spawns AIS-catcher (listening on the 162 MHz marine
channels), receives NMEA over UDP, decodes it with pyais, and plots vessels on
the map with a sortable Vessels list (name/MMSI, speed, course, distance).
AIS works with an ordinary VHF/whip antenna if you're near water. Vessel popups
show the ship type (cargo/tanker/passenger/fishing/sailing/…) once a static
message arrives, and each vessel draws a track trail as it moves. Markers are
colour-coded by ship type (MarineTraffic-style: cargo green, tanker red,
passenger blue, …) and shaped by motion — an arrow pointing to the vessel's
heading when underway, a circle when moored. A collapsible colour legend sits in
the AIS panel.
AIS-catcher isn't in Homebrew, so build it from source once:
brew install cmake
git clone --depth 1 https://github.com/jvde-github/AIS-catcher.git ~/.local/src/AIS-catcher
cd ~/.local/src/AIS-catcher && mkdir build && cd build
# On recent macOS the CLT libc++ headers can be incomplete; point clang at the SDK's:
export CPLUS_INCLUDE_PATH="$(xcrun --show-sdk-path)/usr/include/c++/v1"
cmake .. && make -j4
cp AIS-catcher /opt/homebrew/bin/AIS-catcherPrivacy: AIS-catcher shares received data to aiscatcher.org by default. We launch it with
-X offso nothing leaves your machine.
Select APRS: the backend pipes rtl_fm (NBFM audio at 144.800 MHz, the EU
APRS frequency) into direwolf (the standard
soundcard TNC), parses the decoded TNC2 packets with aprslib, and plots stations
on the map with a Stations list (callsign, info, distance) and a track trail.
A Packets feed in the APRS panel also shows each decoded packet as it arrives
— messages (with recipient), status, weather and position comments, newest first —
so you can read the actual traffic, not just the aggregated map.
Works with an ordinary VHF/whip antenna, but beacons are infrequent (minutes
apart) so give it time. Install direwolf once:
brew install direwolf # rtl_fm comes with the rtl-sdr packagedirewolf 1.8+ won't start without a config file, so the mode ships a minimal
receive-only one at backend/app/modes/direwolf.conf
(loaded via -c); no setup needed.
APRS in North America is 144.390 MHz — change the Center frequency (the mode defaults to the EU 144.800). One RTL-SDR can't do APRS and listen to FM at the same time; APRS is its own mode.
Select ACARS: the backend spawns acarsdec
(watching the common EU channels 131.725 / 131.525 / 131.825 MHz at once), which
emits one JSON message per decode over UDP; we parse them, de-duplicate, and show
a live message log (time · flight/registration · label · text). ACARS carries
no position, so it's a feed rather than a map. Messages are short and infrequent —
best near an airport with a decent airband antenna.
acarsdec isn't in Homebrew; build it (and its libacars dependency) once:
brew install cmake libusb librtlsdr
# libacars (decodes the message contents)
git clone https://github.com/szpajder/libacars ~/.local/src/libacars
cd ~/.local/src/libacars && mkdir build && cd build && cmake .. && make -j4 && sudo make install
# acarsdec
git clone https://github.com/TLeconte/acarsdec ~/.local/src/acarsdec
cd ~/.local/src/acarsdec && mkdir build && cd build
cmake .. -Drtl=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_C_FLAGS="-I/opt/homebrew/include"
make -j4
ln -sf "$PWD/acarsdec" /opt/homebrew/bin/acarsdec # or set ACARSDEC_BIN to the pathmacOS build notes (Apple Silicon, CMake 4): the extra
cmakeflags above are required —CMAKE_POLICY_VERSION_MINIMUM=3.5(CMake 4 dropped acarsdec's oldcmake_minimum_required) and-I/opt/homebrew/include(sortl-sdr.his found). Two source edits are also needed on macOS: defineHOST_NAME_MAX(255) inacarsdec.c, and replace the Linux-onlypthread_tryjoin_npinrtl.c(e.g. poll a done-flag, thenpthread_join). The backend finds the binary onPATHor in~/.local/src/acarsdec/build; setACARSDEC_BINto point anywhere else.
North America centres on 131.550 MHz (plus 130.025/131.725). Edit
CHANNELSin backend/app/modes/acars.py for your region; all channels must fit inside one ~2.4 MHz capture. The mode passes-j host:port(JSON over UDP) to acarsdec — if your build differs, adjust the flags there.
Select APT and pick a satellite (NOAA 15 137.620, NOAA 18 137.9125, NOAA 19 137.100 MHz). During a pass, the image builds top-down at 2 lines/s (~10–15 min for a full pass); Save PNG downloads the full-resolution image, Clear restarts. It's a hand-written decoder (no external tool): FM-demod → AM-detect the 2400 Hz subcarrier → 4160 px/s → sync each 2080-px line → image.
You also get both ways to capture: live (above), or record then decode — hit Record IQ during a pass, then later open Replay, tick "Decode as APT image", and play the recording back through the decoder.
137 MHz needs a satellite pass overhead (use a tracker like gpredict or n2yo.com for pass times) and a proper antenna — the dipole kit in a horizontal "V" (~120°), elements at ~53 cm. The stock whip will barely work. Meteor-M (digital LRPT) is not supported — this is analog NOAA APT only.
Select SSTV: it listens on the 2 m SSTV calling frequency 144.500 MHz (NBFM) and decodes any picture it hears. The mode is auto-detected from the transmission's VIS header — the RGB modes Martin M1/M2 and Scottie S1/S2/DX, plus the YUV modes Robot 36/72 and PD 50/90/120/160/180, are supported. The image builds top-down over ~1–2 min; Save PNG downloads it full-resolution, Clear restarts. The Frequency selector also offers the ISS downlinks (437.550 / 145.800 MHz) for ARISS SSTV events, with a wider filter that absorbs the Doppler drift over a pass. It's a hand-written decoder (no external tool): recover the instantaneous tone frequency (1500 Hz black … 2300 Hz white) → detect VIS → slice each line's colour sweeps → image (YUV modes are converted back to RGB).
For HF SSTV (e.g. 14.230 MHz, an HF upconverter required for an RTL-SDR), open Radio, switch the demod to USB, tune the signal, and tick Decode SSTV image in the Radio panel — the same decoder runs. Record IQ during a transmission to decode it again later in Replay, the same way.
Select Pager: the backend pipes rtl_fm (NBFM audio) into
multimon-ng, which decodes
POCSAG (512/1200/2400 baud) and FLEX; messages stream into a live feed
(newest on top). Pick the channel from the dropdown — DAPNET 439.9875
(the amateur-radio POCSAG network) plus common EU/VHF POCSAG frequencies.
brew install multimon-ng # rtl_fm ships with rtl-sdrWhat's on the air — and whether you may listen to it — varies by country. Use this for the amateur DAPNET network and other lawful, unencrypted traffic.
Select ISM: the backend spawns rtl_433
on 433.92 MHz and forwards each decode to the browser. The view is grouped by
device — one card per transmitter (model · id · channel) with a hit count,
last-seen time and signal level. Each numeric reading (temperature, humidity,
pressure, wind, rain, TPMS pressure…) gets a live sparkline trend with its
current value and min–max range; flags and text show as chips. Devices and their
trends are cached on disk, so they persist across mode switches and restarts. A
type filter narrows the list to one sensor model, and × removes a device
from the view and the cache. A Band selector switches between the ISM bands
(315 / 433.92 / 868.3 / 915 MHz) — picking one relaunches rtl_433 on that
frequency. Switching modes kills rtl_433 and frees the dongle. Gain/PPM are
passed through to rtl_433.
The 433.92 MHz band is full of cheap one-way transmitters: weather stations,
soil/pool/fridge sensors, TPMS tyre-pressure monitors, door/window contacts,
remotes and energy meters — rtl_433 knows hundreds of protocols. Devices beacon
periodically, so leave it running a minute; the band is busiest in the evening.
rtl_433 is in Homebrew:
brew install rtl_433A short whip works fine at 433 MHz (λ/4 ≈ 17 cm); use a shorter element for 868/915 MHz. 433.92 and 868.3 MHz are the EU favourites; 315 and 915 MHz are common in the Americas. Set
RTL_433_BINto point at the binary if it isn't onPATH, andISM_CACHE_PATHto relocate the device-history cache.
FM de-emphasis defaults to 50 µs (Europe); switch it to 75 µs (Americas/Korea) in the Radio panel when listening to WFM broadcast.
A single RTL-SDR has one tuner, so one mode runs at a time — you pick what the dongle is doing. Add a second dongle later for concurrent modes.
RTL-SDR (USB) ──IQ──▶ Python backend (FastAPI)
• DeviceManager owns the dongle (one mode at a time)
• IQ modes run in a worker thread (read_samples + numpy DSP)
• subprocess modes wrap dump1090 / AIS-catcher
└─ WebSocket: JSON control + status, binary FFT/audio
│
▼
Web frontend (Vite + TypeScript)
• waterfall (canvas) • Web Audio (planned)
• Leaflet map (planned)
Key files: backend/app/device.py (device ownership + worker thread), backend/app/modes/ (one file per mode), backend/app/dsp/ (hand-written DSP), frontend/src/ (UI, WebSocket, renderers).
brew install rtl-sdr python@3.12 nodertl-sdr pulls in librtlsdr. Verify the dongle is seen:
rtl_test # should print your tuner (e.g. R820T); Ctrl-C to stoplibrtlsdr / pyrtlsdr note: we pin
pyrtlsdr==0.3.0(see backend/requirements.txt). Newer pyrtlsdr hard-binds a symbol the Homebrewlibrtlsdrdoesn't export and fails to import; 0.3.0 works with the stock library. 0.3.0 in turn wantspkg_resources(gone from setuptools 81+) only to read its own version string, whichapp/device.pystubs out — so no setuptools pin is needed.
# Backend
cd backend
python3.12 -m venv .venv # or: /opt/homebrew/opt/python@3.12/bin/python3.12
./.venv/bin/pip install -r requirements.txt
# Frontend
cd ../frontend
npm installTwo terminals:
# 1) backend (http://localhost:8000)
cd backend && ./.venv/bin/uvicorn app.main:app --reload --port 8000
# 2) frontend (http://localhost:5173 — proxies /api and /ws to the backend)
cd frontend && npm run devOpen http://localhost:5173, click Radio, and you should see the waterfall. Set the Center (MHz) to a strong local FM station, click Tune dongle, then click the signal to listen.
Build the frontend once; the backend then serves it directly — no Vite needed:
cd frontend && npm run build # outputs frontend/dist/
cd ../backend && ./.venv/bin/uvicorn app.main:app --port 8000
# open http://localhost:8000Tip: during development, if you edit the WebSocket framing and the browser behaves oddly, do a hard reload — Vite's HMR can keep a stale module. The single-process build above sidesteps this entirely.
To use Cascade SDR from another computer (or a phone/tablet) on your network, build the frontend and bind the backend to all interfaces:
cd frontend && npm run build
cd ../backend && ./.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000Then browse to http://<this-machine-LAN-IP>:8000 from the other device
(e.g. http://192.168.1.50:8000). The frontend connects its WebSocket back to
whatever host served the page, so no extra config is needed. On macOS, accept the
one-time firewall prompt for python/uvicorn.
Audio over plain HTTP works. Browsers only allow the modern
AudioWorkletin a secure context (HTTPS orlocalhost), so over a plain-HTTP LAN address the player automatically falls back to aScriptProcessorNodewith the same jitter buffer — you still get sound. For HTTPS (and the modern path), run uvicorn with--ssl-keyfile/--ssl-certfile.
No authentication. Binding
0.0.0.0exposes the receiver to your whole LAN. That's fine on a trusted home network — but don't port-forward it to the open internet without putting auth / a reverse proxy in front.
Prefer a real app window over a browser tab? After building the frontend, launch
Cascade SDR in a native OS window via pywebview
(uses the system web view — Cocoa/WebKit on macOS, WebView2 on Windows):
cd frontend && npm run build # build the UI once
cd ../backend && ./.venv/bin/pip install -r requirements-desktop.txt
./.venv/bin/python -m app.desktop # opens the app windowIt starts the backend on 127.0.0.1:8000 in the background and points the window
at it; closing the window shuts everything down. pywebview is an optional
extra — the browser/server runs above don't need it.
The backend and frontend are the same on Windows; only the install differs.
-
Python & Node — install Python 3.12 (tick Add python.exe to PATH) and Node LTS.
-
RTL-SDR driver — Windows has no default libusb driver for the dongle. Run Zadig, choose Options → List All Devices, select Bulk-In, Interface (Interface 0) (the RTL2832U), and install the WinUSB driver. (If you ever want the dongle back as a TV tuner, uninstall it in Device Manager.)
-
librtlsdr — grab a Windows build (e.g. from the osmocom/rtl-sdr binaries or via vcpkg) and put its DLLs on your
PATH. Verify withrtl_test.exe(should list your R820T tuner). -
Set up the app (PowerShell):
# Backend cd backend py -3.12 -m venv .venv .\.venv\Scripts\pip install -r requirements.txt # Frontend cd ..\frontend npm install
-
Run it the same way as above, with Windows paths:
# dev: two terminals cd backend; .\.venv\Scripts\uvicorn app.main:app --reload --port 8000 cd frontend; npm run dev # open http://localhost:5173 # or single-process cd frontend; npm run build cd ..\backend; .\.venv\Scripts\uvicorn app.main:app --port 8000 # http://localhost:8000 # or native window .\.venv\Scripts\pip install -r requirements-desktop.txt .\.venv\Scripts\python -m app.desktop
External decoders (optional): use Windows builds of
dump1090 (ADS-B),
AIS-catcher (AIS, ships a
Windows binary), and welle.io (DAB — welle-cli).
Put them on your PATH so Cascade SDR can launch them. No code changes are needed;
the same -X off / JSON handling applies.
Only one program can own the dongle at a time — close other SDR apps (SDR#, SDRangel, etc.) before running Cascade SDR.
Because the backend owns the dongle and does the DSP while the frontend is just a browser talking over WebSocket, you can run the backend on a Raspberry Pi (with the dongle plugged into the Pi) and use it from any browser on your network — laptop, phone, tablet. Audio is decoded on the Pi and played in your browser, so the Pi needs no sound hardware.
Use a Pi 4 or 5 with a 64-bit OS — the DSP runs at 2.4 MS/s. On a Pi 3, lower the sample rate (e.g. 1.024 MS/s).
numpy/scipyinstall quickly from piwheels.
The backend does real-time DSP at 2.4 MS/s (FFT + FM/AM/SSB demod) and can run a decoder subprocess alongside it, so CPU and a clean, well-powered USB bus matter more than RAM. You supply the RTL-SDR dongle and antenna; the notes below are for the host.
| Tier | Board | Why |
|---|---|---|
| Recommended | Pi 5, 4 GB | Live Radio/Sweep DSP + a decoder at once, no sweat. 8 GB is overkill; 2 GB works. |
| Solid value | Pi 4, 2–4 GB | The baseline above. Fine for normal use; a bit less headroom for "live DSP + heavy decoder" together. |
| Budget | used Pi 4, 2 GB | Cheapest that still runs everything well — just pair it with a proper PSU. |
| Decoders only | Pi Zero 2 W | Too weak for the live waterfall/Radio DSP, but fine as a headless ADS-B / AIS / ISM box at a reduced sample rate. |
| Left-field | used mini PC / N100 | x86, far more DSP headroom than any Pi, no PSU/cooling fuss — often better value than a full Pi 5 kit. Same apt/venv setup. |
Whatever you pick, budget for these — they're where SDR-on-Pi setups usually go wrong:
- A proper power supply. Official 27 W (Pi 5) or 15 W / 5V·3A (Pi 4) USB-C. Underpowering causes USB brown-outs that silently drop the tuner — the #1 failure mode. Don't run the dongle off a phone charger.
- Active cooling. A Pi 5 (and a Pi 4 under sustained load) will thermally throttle the DSP without a fan/heatsink.
- A short USB extension cable so the dongle sits ~30 cm away from the Pi/SSD — noticeably lowers RFI. On a Pi 4, also prefer a USB 2.0 (black) port; the blue USB 3.0 ports emit hash that desensitises the SDR.
- 64-bit OS (Raspberry Pi OS Lite 64-bit is ideal — headless, low overhead), Ethernet for an always-on LAN appliance, and an A2 microSD (or boot from a USB SSD if you'll record IQ a lot).
- If your dongle is an RTL-SDR Blog V4, install the
rtl-sdr-blogdriver fork, not the stockrtl-sdrpackage — a V4 won't tune correctly on the old driver (a V3 is fine with stock drivers).
# 1) dependencies
sudo apt update
sudo apt install -y rtl-sdr librtlsdr-dev python3 python3-venv
rtl_test # confirm the tuner is seen (Ctrl-C to stop)
# Node 20+ is required to build the frontend, and Raspberry Pi OS bookworm
# still ships Node 18 in apt — which fails with "ReferenceError: CustomEvent is
# not defined". Install a current one from NodeSource instead:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v # must be >= 20.19
# If rtl_test says the device is "usb_claim_interface error", the kernel DVB-T
# driver grabbed it. Blacklist it once and reboot:
# echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtl.conf
# sudo reboot
# 2) get the code + set up
git clone https://github.com/rzfk2v/Cascade-SDR.git ~/Cascade-SDR
cd ~/Cascade-SDR/backend
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
cd ../frontend && npm install && npm run build # backend serves this
# 3) run, bound to the whole LAN (note --host 0.0.0.0, not just a port)
cd ../backend && ./.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000Then from any device on the same network open http://<pi-ip>:8000 (find the
Pi's address with hostname -I). That's the only change needed for network use —
--host 0.0.0.0 makes it reachable; the frontend automatically connects back to
whatever host served it.
Each decode mode shells out to an external tool. Cascade finds them on your
PATH and launches them itself, so you just need the binary installed — and
only the ones for the modes you'll use. Install them on the Pi as follows.
ISM (rtl_433) — in the Debian repos (note the dash in the package name):
sudo apt install -y rtl-433APRS (direwolf + rtl_fm) — rtl_fm ships with the rtl-sdr package:
sudo apt install -y direwolf rtl-sdrADS-B (dump1090) — not in the default repos (dump1090-fa is a FlightAware
package). Build it from source; Cascade spawns its own copy, so a plain binary on
PATH is all you need (no background service competing for the dongle):
sudo apt install -y build-essential librtlsdr-dev pkg-config libncurses-dev
git clone https://github.com/flightaware/dump1090.git ~/dump1090-fa
cd ~/dump1090-fa && make
sudo cp dump1090 /usr/local/bin/dump1090Alternatively use FlightAware's apt repo (
install_piaware_repository.sh, thensudo apt install dump1090-fa) — but thensudo systemctl disable --now dump1090-faso its auto-started service doesn't hold the dongle.
AIS (AIS-catcher):
sudo apt install -y git cmake build-essential pkg-config librtlsdr-dev libusb-1.0-0-dev
git clone https://github.com/jvde-github/AIS-catcher.git ~/AIS-catcher
cd ~/AIS-catcher && mkdir build && cd build
cmake .. && make && sudo make installCascade passes
-X off, so received AIS is not uploaded to aiscatcher.org.
DAB (welle-cli):
sudo apt install -y git build-essential cmake xxd libfftw3-dev libusb-1.0-0-dev \
libfaad-dev libmpg123-dev librtlsdr-dev libsndfile1-dev libmp3lame-dev
git clone https://github.com/AlbrechtL/welle.io.git ~/welle.io
cd ~/welle.io && mkdir build && cd build
cmake .. -DRTLSDR=ON -DBUILD_WELLE_IO=OFF -DBUILD_WELLE_CLI=ON
make -j4 welle-cli
sudo cp welle-cli /usr/local/bin/welle-cli
-DBUILD_WELLE_IO=OFFskips the Qt6 GUI (which you don't need and which would otherwise fail to configure without Qt6 installed);welle-cliembeds a small web UI, so it needsxxdat build time (split into its own package on Bookworm).
ACARS (acarsdec + libacars):
sudo apt install -y git cmake build-essential librtlsdr-dev zlib1g-dev libxml2-dev
git clone https://github.com/szpajder/libacars.git ~/libacars
cd ~/libacars && mkdir build && cd build && cmake .. && make && sudo make install && sudo ldconfig
git clone https://github.com/TLeconte/acarsdec.git ~/acarsdec
cd ~/acarsdec && mkdir build && cd build && cmake .. -Drtl=ON && make
sudo cp acarsdec /usr/local/bin/On the Pi you don't need the macOS portability patches mentioned in the ACARS section above — those are Apple-Silicon-only.
If a build stops on a missing header, the error names the lib*-dev to
apt install; install it and re-run make. After installing, just switch to the
mode in Cascade — it spawns the tool on demand and frees the dongle when you
switch away.
To make it a true appliance — power on the Pi, connect from any browser — install the bundled service:
# edit the three paths/user marked in the file first, then:
sudo cp deploy/cascade-sdr.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cascade-sdr
systemctl status cascade-sdr # verify
journalctl -u cascade-sdr -f # follow logsSee deploy/cascade-sdr.service. It restarts on crash and starts after the network is up.
Updating a deployment. Pull, rebuild the frontend, and restart the service in one step with the bundled script:
~/Cascade-SDR/deploy/update.shIt only reinstalls backend/frontend dependencies when they actually changed. Set
SERVICE=<name> if you named the unit something other than cascade-sdr.
If you already run nginx on the Pi (e.g. for another app), you can put Cascade
behind it as a subpath. The bundled
deploy/nginx-sdr.conf is a ready-to-paste location
block that proxies /sdr/ to the uvicorn backend on port 8000, including the
WebSocket upgrade for real-time data:
# paste the contents of deploy/nginx-sdr.conf into your existing
# server { } block, then:
sudo nginx -t && sudo systemctl reload nginxThe frontend build is path-relative and derives its WebSocket/API prefix
from the page URL at runtime, so the same build works served directly by the
backend (http://<host>:8000) and behind the /sdr/ subpath — no build
flags, no backend changes. Nginx strips the /sdr prefix before forwarding.
Security: Cascade SDR has no authentication. Keep it on your trusted LAN, or reach it over a VPN / SSH tunnel (
ssh -L 8000:localhost:8000 pi@<pi-ip>). Do not port-forward it to the public internet as-is.
Cascade SDR is © 2026 Jens Engfors, licensed under GPL-3.0 (see LICENSE).
It builds on other open-source projects — see CREDITS.md for the
full list and their licenses. Provided as-is, without warranty.
Cascade SDR is a receiver for lawful use. Radio-reception rules vary by country: receiving some transmissions, and especially decoding or divulging non-broadcast communications, may be restricted where you live. You are responsible for complying with your local regulations. Map tiles are © OpenStreetMap contributors.




