← Back to list

I Spy With My Software-Defined Radio

Discovering the invisible world of signals surrounding us every day.

Michael Preston in Radio Hackers · 2026-06-04 09:33 · 27 claps · 6.7 min read paywalled
#spying #radio #rf-security #signal #hacking
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 🔒 · Cybersecurity 🎵 · Music & Audio

I Spy With My Software-Defined Radio

Discovering the invisible world of signals surrounding us every day.

Google AI studio by author

Google AI studio by author

1. The First Lesson

The first thing software-defined radio taught me was that the air is never empty. It only looks empty if you do not know how to listen. Once I started sweeping bands with a cheap receiver and a reasonable antenna, I realised how much of the world is constantly speaking in short bursts, carriers, tones, and structured packets.

That changed how I think about systems. SDR is not just a hobby tool. It is a way to see the hidden structure of radio-based infrastructure: aircraft beacons, weather data, remote sensors, paging systems, telemetry links, and a long list of signals that most people never notice because they are not meant for human ears. The software part matters because it turns signal reception into a programmable problem. The hardware only collects samples. The code decides what becomes visible.

from dataclasses import dataclass

@dataclass(frozen=True)
class SignalSnapshot:
    center_freq_hz: int
    sample_rate_hz: int
    gain_db: float
    timestamp: float
    power_dbfs: float

That small model is the right way to think about the work. SDR becomes useful when you stop seeing “a radio” and start seeing a stream of samples that can be measured, filtered, classified, and tracked.

2. The Hardware Is Only The Beginning

A lot of people start with the wrong expectation. They buy a dongle, install a decoder, and assume the rest will just happen. It usually does not. The receiver is only the entry point. The antenna, cable loss, local interference, front-end gain, and sampling settings do most of the real work.

That is why the first useful skill is not decoding. It is signal hygiene. A badly placed antenna can make a great receiver look broken. A noisy USB port can bury weak transmissions. A gain setting that looks fine on paper can destroy dynamic range in practice. I learned to treat the front end like a measurement system, not a toy.

SDR_CONFIG = {
    "center_freq_hz": 433_920_000,
    "sample_rate_hz": 2_000_000,
    "gain_db": 28,
    "ppm_correction": 0,
    "bias_tee": False,
}

That kind of configuration only works if the rest of the chain is sane. I spend more time on antenna placement than on software when the goal is stable reception. The receiver can only amplify what the environment allows it to hear.

3. Waterfalls Tell The Truth

The spectrum display is where SDR stops being abstract. A waterfall turns invisible energy into shape, rhythm, and repetition. You start to recognise narrow carriers, wide bursts, hopping patterns, and recurring pulses. That is the moment the hobby becomes an investigation.

I do not trust a decoder until I have looked at the waterfall. The reason is simple: the shape tells you whether the signal is clean, clipped, drifting, or buried in interference. A waterfall also tells you when your assumptions are wrong. A signal you thought was narrow may actually be frequency-agile. A burst you thought was random may be periodic enough to predict.

import numpy as np

def estimate_power(samples: np.ndarray) -> float:
    if samples.size == 0:
        return float("-inf")
    power = np.mean(np.abs(samples) ** 2)
    return 10 * np.log10(power + 1e-12)

That tiny function does not solve the full problem, but it gives you a starting point. Once you can measure power consistently, you can compare locations, antennas, and tuning windows instead of guessing.

The waterfall also teaches patience. Signals do not always repeat on your schedule. You wait, sweep, and watch. That waiting is not wasted time. It is part of the method.

4. Sampling Is The Real Interface

The biggest conceptual shift in SDR is that the radio becomes software once the signal is digitised. Before that, you are dealing with analog physics. After that, you are dealing with arrays, time windows, filters, and transforms.

That is why I stopped thinking in terms of channels and started thinking in terms of sample streams. The important questions become: what sample rate do I need, what bandwidth am I covering, what resolution do I lose, and how much data can I realistically process in real time?

def iq_to_magnitude(iq: np.ndarray) -> np.ndarray:
    return np.sqrt(iq.real ** 2 + iq.imag ** 2)

def normalize(samples: np.ndarray) -> np.ndarray:
    peak = np.max(np.abs(samples))
    if peak == 0:
        return samples
    return samples / peak

These utilities are basic, but they are the foundation. You cannot analyse what you do not normalise. You cannot classify what you have not reduced into meaningful numeric form. SDR is full of tiny decisions like this, and most of them are more important than the fancy decoder you eventually run on top.

5. FFT Is Where Patterns Emerge

The FFT is where the invisible world stops being mysterious and starts being repetitive. Once you transform the sample stream into frequency space, recurring structures become obvious. Carriers stand still. Bursts become blocks. Noise turns into a floor you can measure. That is when the spectrum starts to feel like a map instead of random texture.

I usually keep the FFT pipeline as small as possible. The more layers I add, the easier it is to hide mistakes. A direct transform with a stable window function is often enough to tell me whether I am looking at a useful signal or just an artifact of my own setup.

def spectrum(samples: np.ndarray) -> np.ndarray:
    window = np.hanning(len(samples))
    windowed = samples * window
    fft = np.fft.fftshift(np.fft.fft(windowed))
    return 20 * np.log10(np.abs(fft) + 1e-12)

What matters here is not the math alone. It is the habit of measuring before decoding. I have seen too many people try to identify a transmission from a half-broken demodulator when the FFT would have shown the structure immediately.

The FFT also teaches humility. Not every spike is meaningful. Some are local interference. Some are self-generated. Some are just the side effects of a poor sampling choice. Good SDR work means learning which peaks matter.

6. Decoding Is Only Useful After Filtering

A decoder is valuable only when the signal has already been cleaned enough to make decoding reliable. That means filtering, timing recovery, thresholding, and sometimes simple hysteresis. If the input is unstable, the output will be garbage with a polished label.

This is the part of SDR where engineering discipline matters more than curiosity. I learned to build small pre-processing stages before I ever wrote a decoder. Once the stream is cleaner, the protocol becomes much easier to recognise.

def moving_average(samples: np.ndarray, window: int = 8) -> np.ndarray:
    if window <= 1:
        return samples
    kernel = np.ones(window) / window
    return np.convolve(samples, kernel, mode="same")

def energy_gate(samples: np.ndarray, threshold: float) -> np.ndarray:
    return samples[np.abs(samples) > threshold]

Even crude filtering helps. The goal is not perfect reconstruction. The goal is to reduce enough noise that a meaningful pattern survives. In a lot of real signals, that is all you need to identify modulation, repetition rate, or packet structure.

This is also where I stopped treating every signal the same way. A narrowband telemetry burst needs a different path than a wideband broadcast signal. A frequency-hopping source needs different handling from a steady carrier. Once the preprocessing reflects the signal class, the whole system gets easier to reason about.

7. Time Matters As Much As Frequency

It is easy to become obsessed with frequency bins and forget that signals happen over time. A transmission is not just “at 433 MHz” or “at 1090 MHz.” It also has duration, repetition, gap structure, and burst timing. Some signals become obvious only when you look at when they appear, not where they sit on the spectrum.

That is why I started logging events instead of just snapshots. The moment you keep timestamps, you can see periodic behaviour, duty cycles, and long gaps between transmissions. You can tell whether something is continuous, intermittent, or event-driven.

from datetime import datetime, timezone

def log_event(label: str, frequency_hz: int, power_dbfs: float) -> dict:
    return {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "label": label,
        "frequency_hz": frequency_hz,
        "power_dbfs": power_dbfs,
    }

That kind of record turns a raw signal hunt into a measurable workflow. Once you have time series data, you can compare days, locations, and antennas. You can also spot changes that are invisible in a single capture.

8. Classification Starts With Discipline

Once you have enough captures, the temptation is to jump straight into classification. I think that is usually too early. The better first step is building a disciplined dataset. That means consistent labels, consistent capture settings, and consistent metadata. Without that, every model learns your mistakes.

The best SDR classification work I have seen starts with simple heuristics. Is the signal continuous or bursty? Is it narrow or wide? Does it repeat at a fixed interval? Does it move in frequency? Those are useful questions before anyone reaches for machine learning.

def simple_signal_profile(samples: np.ndarray) -> dict:
    return {
        "mean": float(np.mean(np.abs(samples))),
        "std": float(np.std(np.abs(samples))),
        "peak": float(np.max(np.abs(samples))),
        "rms": float(np.sqrt(np.mean(np.square(np.abs(samples))))),
    }

That kind of profile does not identify a protocol by itself, but it gives you a baseline. I have found that many “mysterious” signals become less mysterious once you compare their profiles side by side.

The important part is restraint. A classification pipeline is only useful when it reflects the actual RF environment. Otherwise, it becomes a fancy way to overfit noise.

9. What The Air Taught Me

The real lesson of SDR was not that I could decode hidden messages. It is that the world is full of structured communication that most people never notice because they are not looking at it in the right domain.

Once you start working with RF as data, the same patterns show up everywhere. Capture first. Measure before you interpret. Separate hardware from software. Keep raw samples. Keep timestamps. Be suspicious of clean-looking output that cannot be traced back to the input. That mindset transfers directly into observability, networking, distributed systems, and even debugging ordinary applications.

def capture_workflow() -> list[str]:
    steps = [
        "tune receiver",
        "stabilize antenna",
        "capture iq samples",
        "inspect waterfall",
        "measure power",
        "filter noise",
        "decode protocol",
        "log events",
    ]
    return steps

That is still how I think about SDR now. Not as a party trick, and not as a black box, but as a disciplined way of learning how information survives a noisy physical world. The air is full of signals. The software just gives you the patience and precision to notice them.


메타데이터
post_id
efd38cf48daf
slug
i-spy-with-my-software-defined-radio-efd38cf48daf
url
https://radiohackers.com/i-spy-with-my-software-defined-radio-efd38cf48daf
canonical_url
https://radiohackers.com/i-spy-with-my-software-defined-radio-efd38cf48daf
author_url
https://medium.com/@michaelpreston515
status
ok
fetched_at
2026-06-12 10:20:10