Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stationdump

Tune a HackRF to an FM broadcast station and dump every data channel it carries — not just the audio. Built for RF CTFs (DEF CON Wireless Village, 5n4ck3y, ShmooCon RF CTF) where the flag hides in a subcarrier, in the RDS RadioText, in an HD Radio data service, or in the audio itself.

What it captures

An FM broadcast signal is a stack of subcarriers multiplexed into one baseband called the composite (a.k.a. MPX). stationdump FM-demodulates the channel and records the whole composite at a high sample rate, so every data channel is preserved in one file:

In the MPX composite Frequency What it is
Mono audio (L+R) 0–15 kHz the program
Stereo pilot 19 kHz phase reference for stereo
Stereo diff (L−R) 23–53 kHz DSB around 38 kHz
RDS / RBDS 57 kHz station name, RadioText, PI, clock, TMC, ODA, RT+
SCA / SCMO 67 & 92 kHz leased subcarriers (reading services, muzak, data)
DARC 76 kHz Data Radio Channel

Alongside the analog composite, a station may also broadcast HD Radio (IBOC)digital OFDM sidebands carrying extra audio programs (HD1–HD4), Program Service Data, station logos, album art, and traffic. That is a separate data plane, decoded with nrsc5 (--hd).

Tracks — capture is split into timestamped segments

A capture is broken into tracks. For a music station the boundaries come from RDS "now playing" changes (RadioText / RT+ title+artist); a max-duration cap (--segment, default 300 s) bounds every segment so a long talk block can't explode the disk. Each track is timestamped and, if a database is configured, its metadata is pushed there. This is what keeps disk bounded on a long capture: each 3–10 min chunk is processed, its blobs shipped to object storage, and its metadata written — then the local copy is pruned.

Boundaries are measured in signal time (samples ÷ rate), so an offline IQ replay segments exactly like a live capture. Without redsea (or with --track-by time) segmentation falls back to fixed --segment intervals.

Blob upload + DB writes happen on a background worker, so a slow upload never stalls the SDR read (which would drop samples).

Outputs

Per track (NNNN = 1-based sequence), on local disk and/or in object storage:

  • trackNNNN_mpx.wav — the full composite at 240 kHz. The money file: RDS, SCA, DARC and the stereo diff are all still in here, decodable offline.
  • trackNNNN_audio.wav — the demodulated, de-emphasized mono audio (48 kHz).
  • trackNNNN.iq — lossless raw IQ (--raw-iq).

Capture-wide, on local disk:

  • rds.jsonl — every decoded RDS group (one JSON object per line) if redsea is installed.
  • hd_aas/, nrsc5.log — HD Radio data services / logos / art (--hd).
  • meta.json — capture parameters + the full track manifest (paths, S3 keys, timestamps, now-playing, capture_id).

Central database + object storage (the weslack deployment)

Structured metadata → PostgreSQL (captures, tracks, rds_groups, artifacts); the big blobs → S3-compatible object storage (SeaweedFS behind https://files.weslack.team, bucket weslack-rf). The DB rows hold the S3 object keys, so you can SELECT a track and fetch its audio. Configure with --db-url/STATIONDUMP_DB_URL and the --s3-*/STATIONDUMP_S3_* settings; with neither, capture is fully local. See the team's Outline runbook for credentials and connection instructions.

Retention (keep disk bounded)

  • --s3-what (default auto = audio,mpx, plus iq if --raw-iq) — which blob kinds to upload; each is deleted locally after a successful upload unless --keep-local.
  • --keep (default all) — local pruning for blobs not sent to S3: all keeps them, audio drops mpx/iq, min deletes all local blobs.

Do we also need to record the audio itself?

Yes — record the audio, and stationdump does by default. In a radio CTF the "data channels" (RDS et al.) are low-bandwidth text, but the payload very often lives in the audio program: SSTV, DTMF, Morse, a modem/RTTY burst, content visible only in the spectrogram, or a tone hidden at 17–22 kHz. You also cannot re-capture a live broadcast, so capture everything the first time. The defaults therefore record audio + full MPX + RDS; add --raw-iq when disk allows and you want a bit-exact fallback, and --hd if the station is HD.

Use --no-audio only when you deliberately want the data channels alone.

Install

Uses uv for everything.

uv sync                     # create the venv + install deps
uv run stationdump --help

External tools (optional but recommended)

  • hackrf tools (hackrf_transfer) — required for live capture.
  • redsea — live RDS/RBDS decode.
  • nrsc5 — HD Radio decode (--hd).

If redsea/nrsc5 are absent, capture still succeeds; the MPX/raw files let you decode later.

Usage

# 60 s dump of 96.5 MHz: MPX + audio + live RDS, local files
uv run stationdump 96.5M

# Long capture, per-track (per song), push metadata + blobs to the team backend.
# Credentials come from the environment (see the Outline runbook / your env file):
#   STATIONDUMP_DB_URL, STATIONDUMP_S3_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET
uv run stationdump 96.5M --duration 3600 --segment 300

# Force fixed 5-minute chunks instead of per-song boundaries
uv run stationdump 88.5M --track-by time --segment 300

# Europe (50 us de-emphasis), data channels only, no audio
uv run stationdump 100.3M --deemph 50 --no-audio

# Also pull HD Radio HD1 data services (implies --raw-iq)
uv run stationdump 98.7M --hd --hd-program 0

# Gain / front-end tuning
uv run stationdump 91.1M --lna 40 --vga 48 --amp

Decode RDS offline from a saved MPX

sox trackNNNN_mpx.wav -t raw -e signed -b 16 -c 1 - | redsea -r 240000

Inspect the composite in a spectrogram

sox trackNNNN_mpx.wav -n spectrogram -o mpx.png  # pilot@19k, RDS@57k, SCA@67/92k

How it works

HackRF ─cs8 IQ─▶ FIR decimate (keep ±100 kHz) ─▶ FM discriminator ─▶ MPX
                                                          │
        ┌─────────────────────────────────────────────────┼───────────────┐
        ▼                          ▼                        ▼               ▼
  trackNNNN_mpx.wav        redsea → rds.jsonl        audio: LPF 15k     now-playing
  (RDS/SCA/DARC/L-R)       + now-playing             → de-emph          drives track
                                                     → trackNNNN_audio  boundaries
                                                          │
                            per-track (background): ──────┴─▶ S3 upload + DB insert + prune

The DSP is block-streaming and stateful (see stationdump/dsp.py), so a live stream flows through without boundary clicks. Track finalization (S3 + DB + prune) runs on a background worker so uploads never stall the capture. --iq-file replays a saved IQ capture through the exact same path for offline work and testing.

Develop / test

uv run pytest            # synthesizes a known FM signal, asserts demod recovers it

Legal

Receiving and recording broadcast FM is fine in most places. Retransmitting, jamming, or decoding services you aren't authorized to is not. Know your local rules. For authorized CTF/lab use.

License

The Unlicense — public domain. Do whatever you want.

About

Tune a HackRF to an FM station and dump all of its data channels (RDS/SCA/DARC via the composite MPX + HD Radio) and the audio program. For RF CTFs.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages