A first scan
By the end of this chapter, Substation has a folder of its own with its settings in it, it has scanned the 2m band, and you have a recording of a transmission it heard, from the air with your receiver or from a test signal without one.
The folder so far
The radio folder as Installing Substation left it: Substation installed in an environment inside it, switched on.
mkdir radio
cd radio
python3 -m venv .venv
source .venv/bin/activate
pip install "substation==0.8.2"
Substation's settings
Substation reads its settings from a file called config.yaml, in the folder you run it from, and runs on its own defaults without one. Ask it to write one, to see what they are:
substation --init
It holds every setting Substation has, each at its default with a comment saying what it does, and every band it ships. You need change nothing in it yet. It refuses to write over a config.yaml that is already there.
A scan of the air
With your receiver plugged in, its antenna attached, scan the 2m band:
Example not checked: it is not run when this site is built.
substation --band amateur_2m
That reaches an RTL-SDR. For another receiver, name it with --device-type: hackrf for a HackRF One, airspy for an Airspy R2, airspyhf for an Airspy HF+ Discovery, or soapy: and its driver's name for another receiver through SoapySDR.
Substation logs what it does, a line at a time. It first measures the noise floor, the level of hiss its receiver hears with nobody transmitting, and says so: Noise floor warmup complete, then Detection enabled. From then on it watches all 161 radio channels of the band at once. When a transmission rises far enough above the noise floor, its radio channel turns on, as Radio channel 121 ON, with the frequency and the signal-to-noise ratio, how far above the hiss the signal is, in decibels. It turns off when the transmission ends, and the recording is saved. Ctrl-C stops the scan.
The 2m band can be quiet, so a first recording may take a while.
A scan of a test signal
Without a receiver, or to try Substation on a transmission you know is there, make one. A short script writes a file of the IQ samples a receiver would hand over: eight seconds of hiss on the 2m band, with a transmission at 145.5 MHz in the middle of it.
You do not need to know Python to use Substation, which runs from commands and a settings file. This script, written in Python, only stands in for a receiver, and you need not read it: copy it as it is into a file named make_signal.py in the radio folder, in any text editor. Later chapters make their test signals the same way.
The script, make_signal.py, to copy as it is
# Makes test-signal.wav: eight seconds of the 2m band as a receiver hands it over,
# hiss with a transmission in the middle, for Substation to scan in place of a receiver.
import wave
import numpy
# The frequency the transmission is sent on, in hertz, and how many seconds it lasts.
frequency = 145_500_000
length = 3
# The frequency the recording is centred on, and how many IQ samples it holds
# a second, as a receiver set for the 2m band hands them over.
centre = 145_000_000
rate = 2_400_000
# Two seconds of hiss first, while Substation measures it, and three after.
start = 2
seconds = start + length + 3
# The hiss is random, but the same every time the script runs.
noise = numpy.random.default_rng(1)
with wave.open("test-signal.wav", "wb") as file:
# Each IQ sample as two 16-bit numbers, I and Q, held as two audio channels.
file.setnchannels(2)
file.setsampwidth(2)
file.setframerate(rate)
# A second at a time, so the script needs little memory.
for second in range(seconds):
# The moment of each IQ sample, in seconds.
t = second + numpy.arange(rate) / rate
# The receiver's own hiss.
hiss = (noise.standard_normal(rate) + 1j * noise.standard_normal(rate)) * 0.01
# A buzz like a voice: overtones of a low note, each softer than the one below.
voice = sum(5.3 / n ** 2 * numpy.sin(2 * numpy.pi * 150 * n * t) for n in range(2, 7))
# Louder and softer six times a second, like syllables.
loudness = numpy.abs(numpy.sin(2 * numpy.pi * 3 * t))
# FM: the sound swings the transmission's frequency to either side of its own.
phase = 2 * numpy.pi * (frequency - centre) * t + loudness * voice
# The transmission, only while it is on, added to the hiss.
on = (t >= start) & (t < start + length)
iq = hiss + numpy.where(on, 0.3 * numpy.exp(1j * phase), 0)
# Written as pairs of 16-bit numbers, I then Q.
pairs = numpy.stack([iq.real, iq.imag], axis=1)
file.writeframes((pairs * 32767).astype("<i2").tobytes())
Run it with the environment switched on, which has everything it needs. It prints nothing, and writes test-signal.wav beside it:
python make_signal.py
Then scan it as Substation scans the air, with three more options: --iq-file, the file to scan in place of a receiver; --center-freq, the frequency it is centred on, in hertz; and --start-time, the moment it counts as having started, which names and dates what Substation records from it.
substation --band amateur_2m --iq-file test-signal.wav --center-freq 145000000 --start-time "2026-10-05 21:00:00"
Every line starts with the date and time, shown here as … because yours will differ. The first lines say what Substation is about to scan: the band's range, its 161 radio channels, and where it records to. Then come the lines a scan of the air prints too: the noise floor measured, detection enabled, and radio channel 121 turning on at 145.5 MHz, 15.3 dB above the hiss, then off when the transmission ends. Substation heard the transmission, and recorded it.
A file is read as fast as Substation can read it, not in real time, and the scan stops at its end, where a scan of the air goes on until Ctrl-C.
Substation scans any IQ file made the same way, a recording of the air made with other software among them: a WAV file whose two audio channels hold I and Q as 16-bit numbers, with more IQ samples per second than the band is wide in hertz. Its --center-freq is the frequency the receiver was tuned to.
Where a recording lands
Recordings go in a folder called audio, beside config.yaml, in a folder for each day and in that a folder for each band:
ls audio/2026-10-05/amateur_2m
The name says what it is: the date and time the transmission began, the band, the radio channel and its frequency, how far above the hiss it started, and where it came from, file_0 for a test signal, where a receiver's recording says rtlsdr_0 for the first RTL-SDR. Play it in any audio player: it is a buzz, rising and falling six times a second, which is what the script sent.
The next chapter, for musicians, sends every recording straight into Subsample. The one after it looks at the bands Substation defines, which of them it records by default, and a band of your own.