USB dongles, walkie-talkies, and the full RF chain — from synthetic filterbanks to real radio energy through the MitraSETI pipeline.
This chapter shows how to test the MitraSETI signal-detection pipeline using real hardware — from no equipment at all (synthetic filterbanks) through a $25 USB receiver, FRS walkie-talkies, software-defined radios that transmit, and optional hydrogen-line reception at 1420 MHz. The goal is simple to state and deep to satisfy: generate real radio energy, capture it, convert it to MitraSETI's Sigproc .fil format, and verify that the pipeline detects what you know you put on the air (or into the file).
Synthetic unit tests and golden files are indispensable. They are also optimistic: every pixel is an assumption. Real signals carry multipath (delayed copies from reflections), bursting interference, quantization and clipping from 8-bit ADCs, local-oscillator drift, non-Gaussian impulsive noise, and calibration errors in frequency and time. Those effects stress exactly the layers MitraSETI depends on — spectral integration, drift search (Taylor tree), RFI rejection (including spectral kurtosis), clustering, and scoring — under conditions your NumPy fixtures may never reproduce. Testing with real devices builds end-to-end confidence and is, frankly, fun: you hear your experiment in a waterfall plot.
Throughout, we give exact product names, approximate prices, and where to buy; regulations vary by country, so Section 10 is mandatory reading before you transmit anything.
Synthetic data hides glue bugs — half the instrument is outside your repository: the antenna, the front end, the digitizer, and the laptop's USB stack. Real hardware forces you to confront frequency-axis errors, sample ordering bugs, window normalization issues, and header mismatches that synthetic generators may never violate.
In software you control the full time–frequency grid. In the field, half the instrument is outside your repository: the antenna, the front end, the digitizer, and the laptop's USB stack. Failures often appear first as subtle inconsistencies:
fch1 / foff so detections appear offset from truth.tsamp that does not match the actual hop between STFT frames.Synthetic generators copied from one working file may never violate those invariants. A capture from a walkie-talkie at a known FRS channel forces a single question: Does the bright feature sit at 462.5625 MHz? If not, you debug the converter, not the search core.
Multipath smears and duplicates energy in frequency and time. Strong out-of-band signals drive front ends into nonlinearity, spreading intermodulation products across your band. Impulsive RFI violates Gaussian noise models. Cheap dongles exhibit DC spikes, IQ imbalance, and gain-dependent noise floors. MitraSETI's spectral kurtosis and clustering stages exist partly because real observatories see exactly this mess. Feeding the pipeline real-world spectrograms validates that those stages still behave sensibly when the noise is not ideal.
When you transmit a controlled drifting tone (where legal and safe) and recover a hit near the designed drift rate, you have verified RF → ADC → IQ → STFT → power → .fil → MitraSETI in one closed story. That is a different statement from "CI passed on synthetic injections," which only proves internal consistency with your own generators.
Turning the abstract pipeline into something you can key up on a handheld makes bandwidth, FM deviation, narrowband vs broadband, and drift tangible. For many engineers, the first time a PTT press paints a horizontal band on a spectrogram is the moment radio stops being equations and becomes physical.
Prices are approximate USD (early 2026); check vendor sites for current tariffs and sales.
What you do: Build filterbank data in Python / NumPy: Gaussian noise, injected tones or drifting tracks, synthetic RFI bursts, slow gain drift. Write a valid Sigproc .fil (header + float32 power array). Run MitraSETI on the file.
Good for: Unit tests, algorithm regression, CI/CD (fast, deterministic, no RF compliance concerns), teaching Taylor tree behavior without a lab.
Workflow: Create power with shape (n_times, n_channels), set tsamp to seconds per row, fch1 and foff in MHz so channel index maps to sky frequency consistently with your STFT-based hardware path later. Use the writer in Section 8. Run:
bashmitraseti search --file synthetic.fil --snr 10
(MitraSETI's CLI requires --file; --snr sets the detection threshold.)
Limitation: No ADC, no analog distortion, no accidental transmission.
CI/CD pattern: Keep three artifacts in version control: (1) a small .fil with a known injection (checked into tests/data/ or generated deterministically from a fixed seed), (2) a Python script that rebuilds that file using the same write_sigproc_filterbank helper as production conversion, and (3) a thresholded assertion on MitraSETI output (e.g. at least one hit above --snr, drift within tolerance). Run mitraseti search --file … -o summary.json in CI if your runner has the binary; otherwise call the Python pipeline API the CLI uses (MitraSETIPipeline.process_file) so you do not fork a subprocess on every push. Synthetic tests should fail when someone changes header parsing, channel ordering, or default search bounds — exactly the regressions that are painful to catch by eyeballing waterfalls.
| Item | Typical product | Approx. price | Where to buy |
|---|---|---|---|
| Dongle | RTL-SDR Blog V4 | ~$35 | RTL-SDR Blog store, Amazon |
| Alternative | RTL-SDR Blog V3 | ~$30–35 | Same; V3 lacks V4's sub-24 MHz path and filtering refinements |
| Antenna upgrade | Discone or band dipole (e.g. 70 cm / 2 m HAM kits) | ~$30–80 | RTL-SDR shop, HAM retailers, Amazon (match SMA to your dongle) |
Specifications (typical):
Software:
rtl_sdr, rtl_power (Osmocom rtl-sdr tools).pyrtlsdr (wraps librtlsdr).Limitations: Receive-only; no metrology-grade frequency (cheap crystal reference → kHz-scale errors possible at UHF); DC offset at center frequency; image responses if gain and filtering are abused.
Driver notes: On Linux, install rtl-sdr rules and often blacklist DVB drivers that grab the same USB ID. On Windows, Zadig + WinUSB is standard for many SDR apps. On macOS, Homebrew can supply librtlsdr and GQRX; point DYLD or your tool at the library if pyrtlsdr cannot find it.
| Item | Detail |
|---|---|
| Product | HackRF One (Great Scott Gadgets) |
| Price | ~$300–350 |
| Resellers | Adafruit, Hacker Warehouse, Mouser, DigiKey |
| Frequency | 1 MHz – 6 GHz |
| Sample rate | Up to 20 Msps (lab use often 1–10 Msps) |
| ADC | 8-bit |
| Key feature | Half-duplex TX and RX (not simultaneous — switch modes) |
| Software | hackrf_transfer, GNU Radio Companion |
Use case for MitraSETI testing: Generate a narrowband or chirping baseband signal, transmit at minimal power (prefer cabled attenuated path), receive on RTL-SDR or second HackRF, convert to .fil, confirm drift and SNR behavior in the pipeline.
| Item | Detail |
|---|---|
| Product | ADALM-PLUTO (Analog Devices) |
| Price | ~$200 (resellers: DigiKey, Mouser, Analog Direct) |
| Frequency | ~325 MHz – 3.8 GHz (edges depend on variant and firmware) |
| Resolution | 12-bit path → often better dynamic range than 8-bit dongles |
| Duplex | Full-duplex on supported configurations — loopback and channel experiments |
| Software | MATLAB, GNU Radio, pyadi-iio (Python) |
| Item | Detail |
|---|---|
| Examples | Motorola T-series, Midland FRS packs, type-approved FRS sets |
| Retail | Amazon, Walmart, outdoor retailers |
| Bands | 462 – 467 MHz UHF (FRS/GMRS channel plan) |
| Power | FRS: commonly ≤ 0.5 W on designated channels (US, license-free) |
| GMRS | Higher power possible; in the US typically requires FCC GMRS license — see fcc.gov |
| Signal | FM voice → broadband compared to a SETI narrow line; ideal for "does real RF make it through?" tests |
MitraSETI may classify the result as terrestrial / RFI-like (near-zero drift, known allocation) — which is success for a hardware sanity check.
Pedagogical point: Your phone is not "noise"; it is structured human-made emission. MitraSETI is designed to live in a world full of such signals.
Objective: Capture FRS channel 1 at 462.5625 MHz, visualize FM voice, convert to .fil, run MitraSETI.
bashpip install pyrtlsdr numpy scipy matplotlib
center_freq: 462_562_500 Hz (US FRS channel 1).sample_rate: start with 250_000 Hz for a narrow spectrogram around the channel; increase to 2.4e6 if you want more context (more RFI visible).gain: try 40 dB; reduce if the spectrum flattens or splatter appears (clipping).pythonfrom rtlsdr import RtlSdr
import numpy as np
sdr = RtlSdr()
sdr.sample_rate = 250_000
sdr.center_freq = 462_562_500
sdr.gain = 40
n_seconds = 10
samples = sdr.read_samples(int(sdr.sample_rate * n_seconds))
sdr.close()
np.save("walkie_iq.npy", samples)
Option A — minimal (close to a raw spectrogram call): after capture, align complex IQ with a centered frequency axis so "up" on the plot is sky frequency:
pythonimport matplotlib.pyplot as plt
import numpy as np
from scipy.signal import spectrogram
samples = np.load("walkie_iq.npy")
fs = 250_000.0
fc = 462_562_500.0
f, t, Sxx = spectrogram(samples, fs=fs, nperseg=1024)
# Center bin ordering for complex input
f = np.fft.fftshift(f)
Sxx = np.fft.fftshift(Sxx, axes=0)
plt.pcolormesh(t, fc + f, 10 * np.log10(Sxx + 1e-20), shading="auto")
plt.ylabel("Frequency (Hz)")
plt.xlabel("Time (s)")
plt.title("Walkie-talkie — press PTT during capture")
plt.colorbar(label="Power (dB)")
plt.tight_layout()
plt.show()
Option B — explicit return_onesided=False: matches the STFT logic used in Section 8 and avoids surprises when comparing plots to .fil channel mapping:
pythonimport matplotlib.pyplot as plt
import numpy as np
from scipy import signal
samples = np.load("walkie_iq.npy")
fs = 250_000
fc = 462_562_500
f, t, Sxx = signal.spectrogram(
samples,
fs=fs,
nperseg=1024,
noverlap=512,
return_onesided=False,
)
f = np.fft.fftshift(f)
Sxx = np.fft.fftshift(Sxx, axes=0)
plt.figure(figsize=(10, 4))
plt.pcolormesh(t, fc + f, 10 * np.log10(Sxx + 1e-20), shading="gouraud")
plt.ylabel("Frequency (Hz)")
plt.xlabel("Time (s)")
plt.title("Walkie-talkie signal")
plt.colorbar(label="Power (dB)")
plt.tight_layout()
plt.show()
Procedure: Run the script, then press PTT on a handset tuned to channel 1. You should see a bright horizontal band near 462.5625 MHz while you speak.
Use iq_to_filterbank + write_sigproc_filterbank from Section 8 (same STFT lengths as your plot for easier debugging). Example:
bashpython iq_to_filterbank.py # if you saved the __main__ block from Section 8
mitraseti search --file walkie_talkie.fil --snr 10 --max-drift 4
Expected result: Detections associated with ~462.5 MHz, drift rate near zero (terrestrial FM). Downstream logic should treat this as human-made / broadband-like rather than a credible extraterrestrial narrow line — the win is that real RF produced structured pipeline output.
| Symptom | Likely cause | What to try |
|---|---|---|
| No band when keying mic | Wrong channel / frequency | Confirm 462.5625 MHz and channel 1 on both units |
| Everything saturated | Gain too high | Lower sdr.gain; move antenna away from strong FM/TV |
| Band offset from expected | Dongle PPM error | Tune center_freq slightly; for drift tests use GPSDO or calibrate against known station |
.fil looks wrong | STFT / header mismatch | Verify tsamp equals hop/fs from Section 8 |
Objective: Transmit a linearly drifting sinusoid (Doppler analogue), receive with RTL-SDR, process with MitraSETI, see non-zero drift in hits. Read Section 10 first; prefer cable + attenuator over antennas.
If instantaneous frequency increases as f(t) = k t (Hz, relative to baseband), phase is φ(t) = π k t² (integral of 2π f(t)). That matches the common implementation 2π × (drift_rate × t² / 2) with drift_rate = k.
hackrf_transfer -t expects signed 8-bit samples interleaved I, Q:
pythonimport numpy as np
fs = 1_000_000
duration = 60
drift_hz_per_s = 0.5
t = np.arange(0, duration, 1 / fs, dtype=np.float64)
phase = np.pi * drift_hz_per_s * t * t
i_wave = np.cos(phase)
q_wave = np.sin(phase)
iq = np.empty(2 * i_wave.size, dtype=np.int8)
iq[0::2] = np.clip(np.rint(127 * i_wave), -128, 127).astype(np.int8)
iq[1::2] = np.clip(np.rint(127 * q_wave), -128, 127).astype(np.int8)
iq.tofile("drift_signal.iq8")
bashhackrf_transfer -t drift_signal.iq8 -f 433000000 -s 1000000 -a 1 -x 15
Use the lowest -x that closes the link; shorter runs reduce exposure. Cable coupling with 30–60 dB attenuation is ideal.
Same capture pattern as Experiment 1, with center_freq near 433 MHz and sample rate ≥ 1 Msps (or resample offline). Save IQ → STFT → .fil via Section 8.
bashmitraseti search --file drift_rx.fil --snr 8 --max-drift 2
Expected result: A narrowband track with inferred drift near 0.5 Hz/s (sign may flip depending on convention and tuning). The Taylor tree (Chapter 03) should resolve this far more cheaply than brute-force linear search.
Objective: Observe LTE-like broadband structure and time variability; see how MitraSETI and spectral kurtosis (Chapter 04) respond.
A stock RTL-SDR does not tune 2.4 GHz without an upconverter or a different SDR. For phone-induced RFI on a dongle, use a cellular downlink band you can receive legally passively.
Example center (not universal — check your operator and local allocations):
pythonfrom rtlsdr import RtlSdr
sdr = RtlSdr()
sdr.center_freq = 739e6 # Example: vicinity of Band 12 downlink; adjust!
sdr.sample_rate = 2.4e6
sdr.gain = 40
samples = sdr.read_samples(int(sdr.sample_rate * 5))
sdr.close()
.fil as before.bashmitraseti search --file lte_rfi.fil --snr 10
Expected result: Multiple broadband features; kurtosis and clustering should help tag or suppress RFI populations versus rare narrowband outliers. Your phone is deliberate structured interference — a feature, not a bug, for this lab.
Objective: No hardware: validate header, data layout, and MitraSETI on your machine.
pythonimport numpy as np
from pathlib import Path
# Reuse write_sigproc_filterbank from Section 8 (copy into this script or import).
rng = np.random.default_rng(42)
n_chans = 1024
n_times = 16
noise = rng.standard_normal((n_times, n_chans)).astype(np.float32)
signal_chan = 500
drift_chans = 2 # total drift across the observation (channels)
snr = 20.0
for t in range(n_times):
ch = signal_chan + int(drift_chans * t / max(n_times - 1, 1))
noise[t, ch] += snr
# Paste write_sigproc_filterbank() from Section 8 here, or import from iq_to_filterbank.py
write_sigproc_filterbank(
"synthetic.fil",
noise,
source_name="SYN_INJECT",
fch1_mhz=1400.0,
foff_mhz=1e-3,
tsamp_s=1.0,
)
If you prefer a self-contained script, paste the write_sigproc_filterbank function from Section 8 into the same file and call it directly (no import).
bashmitraseti search --file synthetic.fil --snr 10 --max-drift 4
Expected result: Detections near channel ~500 with drift consistent with ~2 channels over 16 steps (exact Hz/s depends on foff and tsamp). Use this as a regression: once calibrated, lock expected hit counts and drift bins in CI.
Recommended "serious hobby" bundle:
| Qty | Item | Approx. cost | Where to buy |
|---|---|---|---|
| 1 | RTL-SDR Blog V4 | ~$35 | rtl-sdr.com |
| 1 | HackRF One | ~$320 | Great Scott Gadgets, Adafruit, Mouser |
| 2 | Antennas (433 MHz dipole, wideband telescopic/discone) | ~$30–80 | RTL-SDR shop, HAM suppliers |
| 1 | FRS walkie-talkie pair | ~$30–50 | Amazon, Walmart |
| 1 | SMA cables + step attenuator(s) | ~$40–100 | HAM/RF vendors |
Total: roughly $420–600 depending on antennas and attenuators.
Software stack:
mitraseti search --file …..fil bridge — Section 8; version-control it like instrument firmware.If you prefer blocks over scripts, build a Companion flowgraph: Signal Source (complex sine or chirp) → Throttle (rate matched to device) → HackRF Sink (TX) or File Sink (record complex float32 for offline conversion). For receive, Osmocom Source or RTL-SDR Source → File Sink captures IQ you later load with np.fromfile(..., dtype=np.complex64) (after you confirm the sink's format — GNU Radio often uses gr_complex = interleaved float32 I/Q). The pedagogical win is immediate: you can drag a Frequency Sink beside your chain and watch images, leakage, and clipping before those artifacts ever become a mysterious MitraSETI false alarm. Export the graph as Python once it works; that generated script is easy to diff in code review.
.fil FormatMitraSETI ingests Sigproc filterbank files: a HEADER_START … HEADER_END keyword block, then spectral data. The Rust reader lives in MitraSETI/core/src/filterbank.rs; MitraSETI/scripts/generate_training_data.py shows a reference write_filterbank_file.
The conversion pipeline: (1) IQ samples x[n] at fs → (2) windowed FFT (STFT) → (3) power |X|² per bin → (4) map each FFT bin to sky frequency fc + fbin → (5) write Sigproc header with source_name, fch1, foff, tsamp, nchans, nifs, nbits, tstart.
Data layout note: Sigproc filterbanks are often described as time-major rows of spectra; align with MitraSETI's reader expectations (float32 when nbits=32).
Complete script (iq_to_filterbank.py):
python#!/usr/bin/env python3
"""Convert complex64 IQ numpy array to Sigproc .fil (float32 power, time x freq)."""
from __future__ import annotations
import struct
import numpy as np
from pathlib import Path
def _write_sigproc_string(f, s: str) -> None:
b = s.encode("ascii")
f.write(struct.pack("I", len(b)))
f.write(b)
def _write_kv_int(f, key: str, v: int) -> None:
_write_sigproc_string(f, key)
f.write(struct.pack("i", v))
def _write_kv_double(f, key: str, v: float) -> None:
_write_sigproc_string(f, key)
f.write(struct.pack("d", float(v)))
def write_sigproc_filterbank(
path: str | Path,
power: np.ndarray,
*,
source_name: str,
fch1_mhz: float,
foff_mhz: float,
tsamp_s: float,
tstart_mjd: float = 59000.0,
src_raj: float = 0.0,
src_dej: float = 0.0,
nbits: int = 32,
) -> None:
"""Write Sigproc filterbank. power shape: (n_times, n_chans), float32 recommended."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
if power.ndim != 2:
raise ValueError("power must be 2-D (n_times, n_chans)")
n_times, n_chans = power.shape
if nbits != 32:
raise NotImplementedError("Tutorial uses float32 samples (nbits=32)")
with open(path, "wb") as f:
_write_sigproc_string(f, "HEADER_START")
_write_sigproc_string(f, "source_name")
sb = source_name.encode("ascii")
f.write(struct.pack("I", len(sb)))
f.write(sb)
_write_kv_double(f, "fch1", fch1_mhz)
_write_kv_double(f, "foff", foff_mhz)
_write_kv_int(f, "nchans", int(n_chans))
_write_kv_int(f, "nbits", int(nbits))
_write_kv_double(f, "tsamp", tsamp_s)
_write_kv_double(f, "tstart", float(tstart_mjd))
_write_kv_int(f, "nifs", 1)
_write_kv_double(f, "src_raj", src_raj)
_write_kv_double(f, "src_dej", src_dej)
_write_sigproc_string(f, "HEADER_END")
power.astype(np.float32).tofile(f)
def iq_to_filterbank(
iq: np.ndarray,
fs_hz: float,
fc_hz: float,
nperseg: int = 1024,
noverlap: int = 512,
) -> tuple[np.ndarray, float, float, float]:
"""
STFT power spectrogram and Sigproc channel mapping.
fch1 = sky frequency of filterbank channel 0 (MHz)
foff = per-channel step from channel i to i+1 (MHz)
tsamp = integration time per spectral row (seconds)
"""
from scipy import signal as sp_signal
if iq.dtype not in (np.complex64, np.complex128):
iq = iq.astype(np.complex64)
f, t, Zxx = sp_signal.stft(
iq,
fs=fs_hz,
nperseg=nperseg,
noverlap=noverlap,
boundary=None,
padded=False,
return_onesided=False,
)
f = np.fft.fftshift(f)
Zxx = np.fft.fftshift(Zxx, axes=0)
power = (np.abs(Zxx) ** 2).T.real.astype(np.float32)
sky_freqs_hz = fc_hz + f
fch1_mhz = sky_freqs_hz[0] / 1e6
if len(sky_freqs_hz) > 1:
foff_mhz = (sky_freqs_hz[1] - sky_freqs_hz[0]) / 1e6
else:
foff_mhz = fs_hz / 1e6
hop = nperseg - noverlap
tsamp_s = hop / fs_hz
return power, fch1_mhz, foff_mhz, tsamp_s
if __name__ == "__main__":
arr = np.load("walkie_iq.npy")
pwr, fch1, foff, tsamp = iq_to_filterbank(
arr, fs_hz=250_000, fc_hz=462_562_500, nperseg=1024, noverlap=512
)
write_sigproc_filterbank(
"walkie_talkie.fil",
pwr,
source_name="WALKIE_FRS1",
fch1_mhz=fch1,
foff_mhz=foff,
tsamp_s=tsamp,
)
print(
f"Wrote walkie_talkie.fil shape={pwr.shape} fch1={fch1:.6f} MHz "
f"foff={foff:.6e} MHz tsamp={tsamp:.6e} s"
)
Sanity checks:
n_times × n_chans × 4 bytes for float32.pwr against fch1 + i · foff and confirm known transmitters line up.The neutral hydrogen (HI) hyperfine line at 1420.405751768 MHz is the same frequency neighborhood as many radio SETI surveys. Proving you can observe HI with your own chain is a strong statement about SETI-relevant hardware. A bare RTL-SDR on a whip may show something on a cold sky vs Galactic plane difference after long integration, but weak science-grade HI usually needs front-end discipline.
The neutral hydrogen (HI) hyperfine line is at 1420.405751768 MHz. It is the same frequency neighborhood as many radio SETI surveys — so proving you can observe HI with your own chain is a strong statement about SETI-relevant hardware.
Reality check: A bare RTL-SDR on a whip may show something on a cold sky vs Galactic plane difference after long integration, but weak science-grade HI usually needs front-end discipline.
Suggested hardware:
| Item | Role | Approx. cost | Where to buy |
|---|---|---|---|
| RTL-SDR Blog V3/V4 | Receiver | ~$35 | RTL-SDR Blog |
| 1420 MHz LNA | Gain before cable losses | ~$20–40 | Nooelec, specialty astro RF shops |
| 1420 MHz bandpass filter | Reject strong out-of-band FM/LTE | ~$50–100 | RF filter vendors |
| Horn or Yagi | Directionality and aperture | DIY cardboard + foil horn (~$0–20 materials) or purchased patch/Yagi |
Procedure (outline):
.fil; run MitraSETI.Expected signature: Not a narrow ET tone — rather a broad, faint excess on the disk-integrated line when comparing on-plane vs off-plane (analysis may be easier in folded spectra or external tools; MitraSETI still validates your file chain). Success means: your entire receive path works at 1420 MHz, which is the spirit of SETI hardware readiness.
Why HI matters for MitraSETI testers: Professional SETI pipelines assume stable passbands, calibrated frequency axes, and RFI mitigation tuned for spectroscopic data. Neutral hydrogen experiments force you to confront bandpass shape, standing waves in cheap coax, and gain stability over long integrations — the same engineering headaches that determine whether a weak narrow line is believable. You do not need a perfect HI detection to win: even a rough excess when pointing differently demonstrates that 1420 MHz energy is entering your digitizer and surviving FFT and file I/O.
Unlicensed ISM and Part 15-style devices are typically milliwatts to tens of milliwatts depending on band and modulation — read local rules. FRS radios are type-approved for specific channels and power. GMRS in the US usually requires a license for legal high-power use. Never treat "experiment" as permission to transmit in aviation, emergency, cellular uplink, or navigation bands.
When unsure, stay receive-only: FRS as transmitter you do not modify, LTE downlink observation, ADS-B, FM broadcast — all useful for pipeline validation without you becoming a transmitter.
Institutional context: Radio regulations exist to protect safety-of-life services and licensed operators. Universities, makerspaces, and employers often impose additional policies on intentional radiators. If you demo MitraSETI in a classroom, receive-only setups with consumer transmitters (FRS, Wi-Fi router across the room captured on a Pluto you own) avoid putting students in the position of unlicensed transmission without a clear lab exception.
| Task | Tool | Example |
|---|---|---|
| Center frequency | pyrtlsdr | sdr.center_freq = 462_562_500 |
| Sample rate | pyrtlsdr | sdr.sample_rate = 2_400_000 |
| Gain | pyrtlsdr | sdr.gain = 40 |
| Auto gain (if supported) | pyrtlsdr | sdr.gain = 'auto' |
| Read IQ | pyrtlsdr | x = sdr.read_samples(1024000) |
| Record IQ (CLI) | rtl_sdr | rtl_sdr -f 462562500 -s 250000 -g 40 walkie.bin |
| HackRF TX file | hackrf_transfer | hackrf_transfer -t drift.iq8 -f 433000000 -s 1000000 -a 1 -x 10 |
| HackRF RX to file | hackrf_transfer | hackrf_transfer -r out.iq8 -f 100e6 -s 2000000 -n 2000000 |
| HackRF sweep (if installed) | hackrf_sweep | hackrf_sweep -f 100:6000 -w 10 |
| GNU Radio: source | GR block | RTL-SDR Source or Osmocom Source |
| GNU Radio: sink | GR block | File Sink (complex gr_complex) |
| GNU Radio: FFT / waterfall | GR block | QT GUI Frequency Sink / Waterfall Sink |
| Visual SDR | GQRX / SDR# | Set frequency, rate, gain; watch waterfall |
| MitraSETI search | CLI | mitraseti search --file obs.fil --snr 10 --max-drift 4 |
| MitraSETI + JSON out | CLI | mitraseti search --file obs.fil -o results.json |
Synthetic .fil files belong in CI; real hardware belongs in your pre-release confidence ritual. Start with Tier 1 injections, add an RTL-SDR and FRS handheld for immediate spectrogram gratification, use HackRF or Pluto only with legal minimal power and preferably cabled attenuation for controlled drift tests, and treat cellular downlink captures as RFI pedagogy. The converter in Section 8 is the bridge between IQ and MitraSETI's Sigproc world — check it in beside your science code. If you outgrow the toy bench, 1420 MHz HI is the aspirational graduation exercise: same band humanity uses to listen to the cosmos and to ask whether anyone answers.