Substation
An SDR band scanner that detects, demodulates, and records radio transmissions automatically.
Connect a USB SDR receiver, point it at a frequency band - Amateur, CB, Airband, PMR, Maritime, or any conventional analogue band - and Substation monitors every radio channel simultaneously, detecting each transmission and recording it to its own audio file with full metadata. Out of the box it records only the bands that UK law opens to anyone, such as amateur and CB radio, and detects activity on the rest: see Reception and the law.
The scanner is designed for unattended, long-running operation. It handles the entire signal processing chain from raw IQ samples through to clean, archive-ready audio files: signal detection, demodulation (NFM, AM, USB, LSB), noise reduction, carrier transient removal, soft limiting, and automatic file management. Noise rejection checks each activation's RF power variance and audio spectral flatness, and each finished recording's length and spectral flatness, and discards what looks like hiss. At present the flatness checks reject hiss only on AM bands: see Limitations. Recordings include embedded metadata - frequency, timestamp, modulation, and any CTCSS or DCS tone detected - so every file is self-documenting.
Substation runs as a command-line tool or as a Python module in your own applications, including on low-power hardware such as a Raspberry Pi scanning a narrower band. The widest shipped bands, at 12.5 MHz, have not yet been shown to keep up in real time, and CTCSS and DCS tone detection has not yet been thoroughly tested with real radios: see Limitations.
Full documentation: https://subsystem.co/substation/
- Guide: https://subsystem.co/substation/guide
- Configuration reference: https://subsystem.co/substation/configuration
- Command-line reference: https://subsystem.co/substation/command-line
- Python reference: https://subsystem.co/substation/reference
The guide starts from what a software-defined radio is and goes as far as a scanner left running, feeding recordings to Subsample and events to Subsequence. The references are generated from Substation's own code, so they describe the release the guide installs.
For changing Substation's code, see docs/architecture.md.
Supported receivers
- RTL-SDR Blog V4 or V3: low cost, for general VHF and UHF listening.
- HackRF One: wideband, from 1 MHz to 6 GHz.
- Airspy R2: high-quality VHF and UHF.
- Airspy HF+ Discovery: HF, and VHF from 64 to 260 MHz, with high sensitivity.
- Any other receiver with a SoapySDR driver module, as
--device-type soapy:<driver>.
Your first receiver compares them, and Setting up your receiver shows how to set each one up.
Quick start
Substation needs your receiver's driver installed first, which the guide's install chapter shows for each receiver. Then:
pip install substation # or "substation[hackrf]" for a HackRF One
substation --init # optional: writes ./config.yaml, the fully commented defaults
substation --band amateur_2m # scans the 2m amateur band with an RTL-SDR
The scanner logs each radio channel on the 2m amateur band as it becomes active, and records each transmission to its own file. The scan is running once the log says Detection enabled. The 2m band can be quiet, so a first recording may take a while. Out of the box only amateur and CB bands record: the rest, such as airband and PMR446, only detect, as the next section explains. Recordings are written to:
./audio/YYYY-MM-DD/<band>/<date>_<time>_<band>_<channel>_<freq>_<snr>dB_<device>_<index>.wav
To install the latest code from GitHub instead, run pip install git+https://github.com/simonholliday/substation.git. From here, the guide goes from a first scan to a scanner left running.
Reception and the law
Many radio services may not lawfully be listened to without permission, and the law differs from country to country. Every band Substation ships carries a reception_class saying how UK law treats it, and the class decides whether the band records out of the box:
| Class | What it covers | Out of the box |
|---|---|---|
general |
What Ofcom calls general reception: licensed broadcasting, amateur and CB radio, and weather and navigation transmissions | Records |
not_general |
Services outside general reception, such as PMR446, business radio, marine, military airband, and emergency services, which Ofcom says it is illegal to listen to | Detects activity without recording |
unsettled |
Bands where the position is unclear, such as civil airband, where Ofcom will not say that listening is an offence | Detects activity without recording |
In the UK, using a receiver to learn the contents, sender, or addressee of a transmission that is not general reception is an offence under the Wireless Telegraphy Act 2006, even if you tell no one. Elsewhere the law differs: the United States, for example, allows receiving unencrypted public-safety, marine, and air radio, and Germany forbids it. The classes describe UK law only, and are not legal advice: the law where you are decides what you may receive and record.
Where your law allows it, switch recording on for a band in your config.yaml:
bands:
air_civil_bristol:
recording_enabled: true
--list-bands shows each band's class and whether it records. A band you define yourself takes its template's class when it sets none of its own, so a band of type: CB records and one of type: PMR only detects. A band with no class records only if you give it reception_class: general or recording_enabled: true.
Limitations
- Processing time grows with the number of IQ samples a second a band takes, and most of it runs on one CPU core. The three shipped bands at 12.5 MHz,
air_civil_1,air_civil_2, anddmr, have not yet been shown to keep up in real time: on the one desktop computer they have been tested on, processing fell behind and IQ samples were dropped, and faster computers are still to be tested. On a Raspberry Pi, or whereverProcessing overrunwarnings appear, scan a narrower band, such as one ofdmr_1todmr_5. - CTCSS and DCS tone detection has not yet been thoroughly tested with real radios, so treat a reported tone as a guide rather than a certainty, and the absence of one as inconclusive.
- The spectral flatness checks reject hiss only on AM bands at present. The NFM, USB, and LSB demodulators filter their audio to the voice band before the checks measure it, and the checks measure the whole audio spectrum, so the empty frequencies outside the voice band make hiss measure as peaked, far below the threshold of 0.15. On those bands, which include every band that records out of the box, both checks keep hiss, and noise rejection rests on the SNR threshold and the RF power variance check. Fixing this needs a measure and a threshold chosen against real recordings of voice and hiss. Recordings of nothing explains each check.
- If you enable
apply_noisereduce(requires a code change and thenoisereduceextra), it is CPU-intensive for long chunks; on constrained devices, stick with the defaultapply_spectral_subtractionor reducedisk_flush_interval_seconds.
Author
Written by Simon Holliday (https://simonholliday.com/)
This project is managed with Subroutine.
Licence
Substation is released under the GNU Affero General Public License v3.0 (AGPLv3).
You are free to use, modify, and distribute this software under the terms of the AGPL. If you run a modified version of Substation as part of a network service, you must make the source code available to its users.
Metadata
Release files for substation 0.8.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| substation-0.8.1.tar.gz | 247.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| substation-0.8.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 423.1 kB
Release files / substation-0.8.1.tar.gz
| Download URL | substation-0.8.1.tar.gz |
|---|---|
| Size | 247.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
94ec53bc156e037164d42c7a08bb96d259c19f786ef9b2eac30b82544f03fba3
|
|
BLAKE2b-256 checksum How to use checksums |
77412f111fbdbd2cef181c40679df7dfefa72cdcacb1636991ef559b90f59604
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logRelease files / substation-0.8.1-py3-none-any.whl
| Download URL | substation-0.8.1-py3-none-any.whl |
|---|---|
| Size | 175.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fc9106c26de4136a4dbcb690ec50c2854926d04d629d3e2392742daa3973536
|
|
BLAKE2b-256 checksum How to use checksums |
5c547bac3364d49b36f3dd98e56cd2026f181589042d5ef1625609435723f249
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency log