Chapter 11

Testing with Real Devices

USB dongles, walkie-talkies, and the full RF chain — from synthetic filterbanks to real radio energy through the MitraSETI pipeline.

Author: Saman Tabatabaeian — Deep Field Labs Level: Practical / Hands-on Prerequisites: Chapters 01, 03, 04, 08

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.

1. Why Test with Real Hardware?

Key Concept — Why Real Hardware Matters

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.

Synthetic data hides glue bugs

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:

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.

Real channels are messy

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.

End-to-end closure

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.

Education and joy

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.

2. Equipment Overview (Cheapest → Most Capable)

Prices are approximate USD (early 2026); check vendor sites for current tariffs and sales.

Equipment Tiers — Cheap to Serious TIER 1 Software Only $0 Python + NumPy Synthetic .fil TIER 2 RTL-SDR ~$35 RX only, 8-bit 24 MHz–1.7 GHz TIER 3 HackRF One ~$320 TX + RX, 8-bit 1 MHz–6 GHz TIER 4 ADALM-Pluto ~$200 Full duplex, 12-bit 325 MHz–3.8 GHz TIER 5 FRS Walkie-Talkies ~$30–50/pair 462–467 MHz UHF FM voice, ≤ 0.5 W TIER 6 Mobile Phone $0 Wi-Fi, BT, LTE Great RFI source Simple / Free Full-featured ← Increasing capability → All tiers contribute to pipeline testing — start with Tier 1, add as budget allows Serious hobby bundle (Tiers 1–5): ~$420–600 total Equipment tiers from free (software-only) to full-featured SDR — every tier contributes to pipeline validation.

Tier 1 — Software-only ($0)

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.

Tier 2 — RTL-SDR receiver ($25–35)

USB SMA TUNER R820T2 ADC 8-bit RTL2832U DVB-T→SDR RTL-SDR Blog V4 Freq: 500 kHz – 1.7 GHz Rate: ~2.4 Msps stable ADC: 8-bit (limited DR) Software: pyrtlsdr, GQRX, SDR# Mode: RX only Price: ~$35 RTL-SDR dongle concept: tuner → 8-bit ADC → USB — a $35 radio telescope front end.
ItemTypical productApprox. priceWhere to buy
DongleRTL-SDR Blog V4~$35RTL-SDR Blog store, Amazon
AlternativeRTL-SDR Blog V3~$30–35Same; V3 lacks V4's sub-24 MHz path and filtering refinements
Antenna upgradeDiscone or band dipole (e.g. 70 cm / 2 m HAM kits)~$30–80RTL-SDR shop, HAM retailers, Amazon (match SMA to your dongle)

Specifications (typical):

Software:

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.

Tier 3 — HackRF One ($300–350)

ItemDetail
ProductHackRF One (Great Scott Gadgets)
Price~$300–350
ResellersAdafruit, Hacker Warehouse, Mouser, DigiKey
Frequency1 MHz – 6 GHz
Sample rateUp to 20 Msps (lab use often 1–10 Msps)
ADC8-bit
Key featureHalf-duplex TX and RX (not simultaneous — switch modes)
Softwarehackrf_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.

Tier 4 — ADALM-Pluto SDR (~$200)

ItemDetail
ProductADALM-PLUTO (Analog Devices)
Price~$200 (resellers: DigiKey, Mouser, Analog Direct)
Frequency~325 MHz – 3.8 GHz (edges depend on variant and firmware)
Resolution12-bit path → often better dynamic range than 8-bit dongles
DuplexFull-duplex on supported configurations — loopback and channel experiments
SoftwareMATLAB, GNU Radio, pyadi-iio (Python)

Tier 5 — Walkie-talkie / FRS / GMRS ($30–50 per pair)

ItemDetail
ExamplesMotorola T-series, Midland FRS packs, type-approved FRS sets
RetailAmazon, Walmart, outdoor retailers
Bands462 – 467 MHz UHF (FRS/GMRS channel plan)
PowerFRS: commonly ≤ 0.5 W on designated channels (US, license-free)
GMRSHigher power possible; in the US typically requires FCC GMRS license — see fcc.gov
SignalFM 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.

Tier 6 — Mobile phone ($0)

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.

3. Experiment 1 — Detect a Walkie-Talkie with RTL-SDR

Objective: Capture FRS channel 1 at 462.5625 MHz, visualize FM voice, convert to .fil, run MitraSETI.

3.1 Setup

  1. Plug the RTL-SDR into a USB 2.0/3.0 port; a short shielded USB extension reduces mechanical stress and heat near the laptop.
  2. Install Python packages:
bashpip install pyrtlsdr numpy scipy matplotlib
  1. Configure the SDR:

3.2 Capture

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)

3.3 Spectrogram (quick visualization)

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.

3.4 Convert to filterbank and run MitraSETI

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.

3.5 Troubleshooting

SymptomLikely causeWhat to try
No band when keying micWrong channel / frequencyConfirm 462.5625 MHz and channel 1 on both units
Everything saturatedGain too highLower sdr.gain; move antenna away from strong FM/TV
Band offset from expectedDongle PPM errorTune center_freq slightly; for drift tests use GPSDO or calibrate against known station
.fil looks wrongSTFT / header mismatchVerify tsamp equals hop/fs from Section 8

4. Experiment 2 — Simulate a Drifting SETI-like Signal with HackRF

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.

4.1 Physics of the baseband waveform

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.

4.2 Generate interleaved 8-bit IQ for HackRF

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")

4.3 Transmit (example: 433 MHz ISM — verify legality locally)

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.

4.4 Receive with RTL-SDR

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.

4.5 Run MitraSETI

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.

5. Experiment 3 — Mobile Phone as an RFI Source

Objective: Observe LTE-like broadband structure and time variability; see how MitraSETI and spectral kurtosis (Chapter 04) respond.

5.1 RTL-SDR and 2.4 GHz

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.

5.2 Example: LTE downlink band (carrier-dependent)

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()

5.3 Procedure

  1. Place the phone beside the antenna.
  2. Airplane mode off; load a heavy web page, run a speed test, or stream video (where safe/legal).
  3. Build a spectrogram and then .fil as before.
  4. Run:
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.

6. Experiment 4 — Software-Only End-to-End Test

Objective: No hardware: validate header, data layout, and MitraSETI on your machine.

6.1 Minimal injection (small time axis)

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).

6.2 Run the pipeline

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.

7. Building a Complete Test Bench

Recommended "serious hobby" bundle:

QtyItemApprox. costWhere to buy
1RTL-SDR Blog V4~$35rtl-sdr.com
1HackRF One~$320Great Scott Gadgets, Adafruit, Mouser
2Antennas (433 MHz dipole, wideband telescopic/discone)~$30–80RTL-SDR shop, HAM suppliers
1FRS walkie-talkie pair~$30–50Amazon, Walmart
1SMA cables + step attenuator(s)~$40–100HAM/RF vendors

Total: roughly $420–600 depending on antennas and attenuators.

Test Bench Setup Diagram FRS RADIO 462 MHz FM voice PTT → HackRF TX 433 MHz ISM drift signal ATTEN 30–60 dB RTL-SDR RX 24 MHz–1.7 GHz 8-bit IQ USB WATERFALL LAPTOP pyrtlsdr + GQRX PIPELINE IQ → STFT → .fil → MitraSETI search RESULTS ✓ Hits detected ✓ Drift matched SMA cable or over-air Complete test bench: FRS radio or HackRF as transmitter, RTL-SDR as receiver, laptop running the full MitraSETI pipeline.

Software stack:

GNU Radio workflow (optional but powerful)

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 SourceFile 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.

8. Converting SDR IQ to Sigproc .fil Format

MitraSETI ingests Sigproc filterbank files: a HEADER_STARTHEADER_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.

Key Concept — IQ to Filterbank Conversion

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.

IQ → Filterbank Conversion Pipeline IQ SAMPLES complex64 x[n] @ fs STFT windowed FFT fftshift |X|² power spectrum float32 FREQ MAP fc + fbin → sky MHz .fil HEADER_START fch1, foff, tsamp MitraSETI search SDR capture Spectrogram Sigproc file Data layout: time-major rows of spectra, float32 when nbits=32 IQ-to-filterbank conversion: from SDR capture through STFT power spectrogram to Sigproc .fil for MitraSETI ingestion.

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:

9. Advanced — Hydrogen Line Detection (1420 MHz)

Fun Fact — The Hydrogen Line

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:

ItemRoleApprox. costWhere to buy
RTL-SDR Blog V3/V4Receiver~$35RTL-SDR Blog
1420 MHz LNAGain before cable losses~$20–40Nooelec, specialty astro RF shops
1420 MHz bandpass filterReject strong out-of-band FM/LTE~$50–100RF filter vendors
Horn or YagiDirectionality and apertureDIY cardboard + foil horn (~$0–20 materials) or purchased patch/Yagi

Procedure (outline):

  1. Mount antenna with a clear view of high Galactic latitude vs plane (e.g. toward Cygnus / Galactic center when possible).
  2. Integrate 10–60 minutes of stable data (minimize gain changes mid-scan).
  3. Convert to .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.

10. Safety and Legal Notes

Warning — Power Limits & Legal Requirements

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.

11. Quick Reference — SDR Commands

TaskToolExample
Center frequencypyrtlsdrsdr.center_freq = 462_562_500
Sample ratepyrtlsdrsdr.sample_rate = 2_400_000
Gainpyrtlsdrsdr.gain = 40
Auto gain (if supported)pyrtlsdrsdr.gain = 'auto'
Read IQpyrtlsdrx = sdr.read_samples(1024000)
Record IQ (CLI)rtl_sdrrtl_sdr -f 462562500 -s 250000 -g 40 walkie.bin
HackRF TX filehackrf_transferhackrf_transfer -t drift.iq8 -f 433000000 -s 1000000 -a 1 -x 10
HackRF RX to filehackrf_transferhackrf_transfer -r out.iq8 -f 100e6 -s 2000000 -n 2000000
HackRF sweep (if installed)hackrf_sweephackrf_sweep -f 100:6000 -w 10
GNU Radio: sourceGR blockRTL-SDR Source or Osmocom Source
GNU Radio: sinkGR blockFile Sink (complex gr_complex)
GNU Radio: FFT / waterfallGR blockQT GUI Frequency Sink / Waterfall Sink
Visual SDRGQRX / SDR#Set frequency, rate, gain; watch waterfall
MitraSETI searchCLImitraseti search --file obs.fil --snr 10 --max-drift 4
MitraSETI + JSON outCLImitraseti search --file obs.fil -o results.json

Summary

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.