Skip to content

About

Cascade SDR — web SDR for RTL-SDR dongles: live spectrum/waterfall, FM/AM/SSB radio with RDS, channel scanner, ADS-B, AIS, APRS, ACARS, DAB+, NOAA APT and ISM-band devices (315–915 MHz: weather sensors, TPMS).

Topics

Resources

Stars

47 stars

Watchers

2 watching

Forks

Latest commit

 

History

143 Commits

Folders and files

Repository files navigation

Cascade SDR

CI

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.

Cascade SDR — the Radio view: live waterfall + scope with click-to-listen

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

AIS — vessels on the map with country flags, and a full per-ship readout (name, MMSI, flag, dimensions, status, ETA…)

Using the Radio view (waterfall + listen in one)

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.

Finding signals with Sweep

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

Scanner (monitor channels, stop on activity)

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.

Scanner — a Marine VHF preset cycling channels, each with a live signal bar, parked on an active channel with a Priority channel set and squelch (dB over noise) control

Replay a recording

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.

Zoom, gain, PPM

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

Demodulators

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.

Extra receivers (VFO B/C/D)

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.

Recording

  • 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; set CASCADE_RECORDINGS_DIR to 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.

Antenna helper (dipole kit)

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.

Display, bookmarks, persistence

  • 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/JSON list (shown as My list), optionally untick Show built-in lists to use only yours, or edit frontend/src/frequencies.ts to 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.

Layout

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.

ADS-B (aircraft map)

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.

ADS-B — aircraft on an OpenStreetMap map as heading-pointed icons sized by class, a distance-sorted Aircraft list with climb/descent arrows, and a popup with full detail (callsign, ICAO, tail #, model, operator, altitude, climb, speed, track)

DAB radio (†)

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-cli

Needs a Band III antenna. Verified live in Stockholm: block 12C = the SR STOCKHOLM ensemble (P1–P4, etc.).

AIS (ships map)

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-catcher

Privacy: AIS-catcher shares received data to aiscatcher.org by default. We launch it with -X off so nothing leaves your machine.

APRS (packet-radio stations map) (‡)

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 package

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

ACARS (aircraft data feed) (§)

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 path

macOS build notes (Apple Silicon, CMake 4): the extra cmake flags above are required — CMAKE_POLICY_VERSION_MINIMUM=3.5 (CMake 4 dropped acarsdec's old cmake_minimum_required) and -I/opt/homebrew/include (so rtl-sdr.h is found). Two source edits are also needed on macOS: define HOST_NAME_MAX (255) in acarsdec.c, and replace the Linux-only pthread_tryjoin_np in rtl.c (e.g. poll a done-flag, then pthread_join). The backend finds the binary on PATH or in ~/.local/src/acarsdec/build; set ACARSDEC_BIN to point anywhere else.

North America centres on 131.550 MHz (plus 130.025/131.725). Edit CHANNELS in 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.

NOAA APT (weather-satellite images) (¶)

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.

SSTV (slow-scan TV images) (◇)

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.

Pager (POCSAG/FLEX) (◆)

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-sdr

What'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.

ISM devices (315–915 MHz) (‖)

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.

ISM — nearby ISM-band devices grouped per transmitter (Bresser & AmbientWeather sensors, Toyota TPMS) with live temperature/humidity sparklines and min–max range, a band selector, a per-type filter, and a × to remove a device

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_433

A 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_BIN to point at the binary if it isn't on PATH, and ISM_CACHE_PATH to 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.

Architecture

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

Prerequisites (macOS, Apple Silicon)

brew install rtl-sdr python@3.12 node

rtl-sdr pulls in librtlsdr. Verify the dongle is seen:

rtl_test        # should print your tuner (e.g. R820T); Ctrl-C to stop

librtlsdr / pyrtlsdr note: we pin pyrtlsdr==0.3.0 (see backend/requirements.txt). Newer pyrtlsdr hard-binds a symbol the Homebrew librtlsdr doesn't export and fails to import; 0.3.0 works with the stock library. 0.3.0 in turn wants pkg_resources (gone from setuptools 81+) only to read its own version string, which app/device.py stubs out — so no setuptools pin is needed.

Setup

# 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 install

Run (development)

Two 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 dev

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

Run (single-process)

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:8000

Tip: 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.

Run (remote access over the LAN)

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 8000

Then 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 AudioWorklet in a secure context (HTTPS or localhost), so over a plain-HTTP LAN address the player automatically falls back to a ScriptProcessorNode with 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.0 exposes 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.

Run (native desktop app)

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 window

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

Windows

The backend and frontend are the same on Windows; only the install differs.

  1. Python & Node — install Python 3.12 (tick Add python.exe to PATH) and Node LTS.

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

  3. librtlsdr — grab a Windows build (e.g. from the osmocom/rtl-sdr binaries or via vcpkg) and put its DLLs on your PATH. Verify with rtl_test.exe (should list your R820T tuner).

  4. 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
  5. 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.

Raspberry Pi / headless Linux (run it as a network appliance)

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/scipy install quickly from piwheels.

Hardware recommendations

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-blog driver fork, not the stock rtl-sdr package — 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 8000

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

External decoders on the Pi (optional)

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-433

APRS (direwolf + rtl_fm) — rtl_fm ships with the rtl-sdr package:

sudo apt install -y direwolf rtl-sdr

ADS-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/dump1090

Alternatively use FlightAware's apt repo (install_piaware_repository.sh, then sudo apt install dump1090-fa) — but then sudo systemctl disable --now dump1090-fa so 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 install

Cascade 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=OFF skips the Qt6 GUI (which you don't need and which would otherwise fail to configure without Qt6 installed); welle-cli embeds a small web UI, so it needs xxd at 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.

Auto-start on boot (systemd)

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 logs

See 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.sh

It only reinstalls backend/frontend dependencies when they actually changed. Set SERVICE=<name> if you named the unit something other than cascade-sdr.

HTTPS with nginx (optional)

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 nginx

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

License

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.

Reception disclaimer

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.

About

Cascade SDR — web SDR for RTL-SDR dongles: live spectrum/waterfall, FM/AM/SSB radio with RDS, channel scanner, ADS-B, AIS, APRS, ACARS, DAB+, NOAA APT and ISM-band devices (315–915 MHz: weather sensors, TPMS).

Topics

Resources

Stars

47 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages