Zkitszo - The Real
August 14, 2026

One Man's Garbage is Another Man's Active Dish Weather Radar

>

Winegard Carryout GM-5000 + HackRF/PlutoSDR: A Salvage-Built Portable Weather Radar

A $5 junkyard RV satellite dome, a hobbyist SDR, and about 1,500 lines of Python turn into a vehicle-mounted X-band weather radar with a live web scope, tiered storm alerts, and a projected storm-track cone. The builder (GitHub user Koakno, Kokomo, Indiana) started the project after nearly getting hit by a tornado that NEXRAD didn't paint at all at the range that mattered — NWS radar data is 4–6 minutes old and a distant station's beam overshoots low storm features entirely. Professional Doppler-on-Wheels trucks solve that for $500,000+. This repo (116 stars, first commit June 2026) does it with items salvaged from ya local garbage/ salvage spot...

Winegard Carryout dome mounted on a pickup roof rack with a supercell on the horizon
The GM-5000 dome: a motorized 18–22" prime-focus dish in a weatherproof radome, rated to 35 mph wind — stow it flat at highway speed.

Contents: Dome → radar · Two SDRs, two timing modes · The honest numbers · Driving the motor · Software & alerts · The FCC problem · Build notes & gotchas · Glossary · Sources

Dome → radar: the LNB upconversion trick

The Carryout Anser GM-5000 is an automatic satellite-TV dome: ~32–33 dBi of dish gain at 10 GHz, 4–5° beamwidth, belt-driven azimuth, manual elevation. Its Eagle Aspen 501353 LNB runs an 11250 MHz local oscillator. Feed a signal into the LNB's IF port and the mixer runs in reverse — inject 850 MHz and the dish radiates at 11250 − 850 = 10.4 GHz, squarely in the X-band range weather radars use. Precipitation reflects some of it back, the LNB downconverts the echo to 850 MHz, and the SDR captures it.

SDR TX (850 MHz) -> MAIN F port -> LNB upconverts -> dish radiates 10.4 GHz
rain/hail echo   -> dish -> LNB downconverts -> SEC F port -> SDR RX

TX injection and RX come out of two physically separate LNB outputs (MAIN and SEC, active simultaneously), so there's no T/R switch and no backfeed path on the dish side. A bias tee on the SEC coax powers the LNB. Note the whole thing runs out-of-spec by design: the LNB's rated input band is 12.2–12.7 GHz, so at 10.4 GHz the feed is working ~2 GHz below its design band — part of why the project's range claims are conservative (below).

💎 Buried detail: the two SDR backends don't radiate the same frequency. The HackRF pulsed path injects 850 MHz → 10.4 GHz, but config.py gives the Pluto FMCW path a 10.25 GHz center (mid-band of the 10.0–10.5 GHz allocation), so it injects 1000 MHz — which also happens to land inside the LNB's rated 950–1450 MHz IF window, while 850 MHz sits 100 MHz below it.
Signal chain diagram from SDR through F-to-SMA adapters, LNB MAIN/SEC ports, dish, and back
Full signal chain. MAIN carries TX injection; SEC is a dedicated clean receive path with LNB power over the coax.

Two SDRs, two radar timing modes — hardware picks, not you

There is no mode setting. At startup RadarSDR.connect() probes for a Pluto/AD9363 first, then a HackRF via SoapySDR, and the board found dictates the radar's entire timing model:

  • HackRF One (half-duplex) — one shared RF front end that physically can't TX and RX at once, so it gets pulsed timing: fire a 9 µs pulse, stop, listen, step the dish 2°, repeat. Port note the docs call out because people get it wrong: ANT is the radar port (TX and RX internally); CLKOUT is only a 10 MHz reference clock, not an RF port.
  • Pluto/AD9363 (full-duplex) — independent TX/RX chains, so it runs a continuous FMCW chirp: 10 MHz sweep (under the AD9363's ~20 MHz RF bandwidth ceiling), 1 ms duration, looped forever via the cyclic TX buffer while RX dechirps in software. Bonus: it talks libiio over Ethernet, so it runs from Android/Termux without USB permission fights.
  • Neither found — the whole stack degrades to a live simulation with two synthetic drifting storm cells, so alerts, tracking, and the display are testable with zero RF hardware. A standalone single-file portable_radar_simulator.html does the same in a bare browser.

The Pluto path carries explicit UNVERIFIED AGAINST REAL HARDWARE comments in sdr.py — treat it as a debugging starting point, not a known-good path.

💎 Buried detail: a comment in dechirp_range_profile() admits the first version of the FMCW math was wrong in a classic way — for an up-chirp, a positive round-trip delay produces a negative beat frequency. The fix is a single sign: ranges_m = -freqs · c / (2 · chirp_rate). If you're building any FMCW dechirper, check your sign before you chase "impossible" negative ranges.

The honest numbers

Rare for a hobby radar repo: the README walks back its own earlier hype. Early 40–60 km range claims assumed near-free-space propagation; the shipped default is MAX_RANGE_KM = 20, citing Earth curvature, terrain/foliage clutter, and the out-of-band LNB's noise figure. Some arithmetic the docs don't spell out: a 9 µs pulse means a ~1.35 km blind zone and similar range resolution; at the HackRF's 2 Msps that's 75 m per sample, binned to 1 km. The pulse-then-listen sequence is two separate SoapySDR stream activations from Python — and at radar timescales, where the full 20 km round trip is 133 µs, every millisecond of host-side latency is 150 km of apparent range. The FMCW path is the answer to exactly that (10 MHz of chirp bandwidth gives c/2B = 15 m theoretical resolution, with the 1 ms chirp a 7.5× margin over the round trip), which is likely why it exists at all — but per the code's own flags, it hasn't ranged a real storm yet either. Calibrate MAX_RANGE_KM to what your hardware actually sees.

Driving the motor: RS-485 behind a knockout panel

The azimuth protocol comes from SaveItForParts' reverse engineering of this dome for radio astronomy. A thin plastic knockout on the base housing pops out with a flathead — it's factory-scored, like the insert between a milk jug's handle and body — exposing an RJ-25 jack (6-conductor, not RJ-11): pin 1 GND, 2 T/R−, 3 T/R+, 4 RXD−, 5 RXD+, 6 NC, at 57600 8N1 through an RS-485 converter (the docs specify the DTECH brand converter).

Close-up of the dome base knockout panel and RJ-25 jack with pinout labels
The RJ-25 control jack hides behind a factory knockout in the base housing.

As implemented in motor.py, frames are FF FF | cmd | data | checksum (checksum = byte sum of cmd+data & 0xFF), with 0x50 move-azimuth (target in tenths of a degree, big-endian uint16), 0x51 stop, 0x52 position query — replies also in tenths of a degree — and 0x53 home. Two field-tested subtleties are baked in: an empty reply is a valid zero-payload ack, distinct from a failed write (callers check is None, not truthiness), and position polls wait 50 ms instead of the serial layer's 1 s timeout so a mute controller can't stall the sweep. The sweep generator also stops one step short on full circles, because sampling both 0° and 360° double-counts one bearing per revolution.

Software & alerts

A Flask server (/api/telemetry, polled at 200 ms) feeds a canvas PPI scope with NWS-style reflectivity colors and offline Indiana county/road overlays — no internet anywhere in the loop. Alerts fire once per sweep, and every tier except TRACK must qualify on 2 consecutive sweeps and then holds a 30 s cooldown, which kills single-sweep noise flicker:

TierTrigger (as coded)
INFOAny precipitation return
CAUTIONStorm core in range
WARNING>40 strong returns (>0.5 normalized) in one sweep
DANGERExtreme-strength return within 5 km
TRACKMulti-sweep storm heading/speed + cone (live motion, not debounced)

The cone of uncertainty is earned, not decorative: storm centroid tracked across sweeps, projected out 5/10/15/20 minutes, with half-angle = min(45°, base + heading-wobble × 1.5) scaled by projection time — a steady storm gets a tight cone, a wobbler gets a wide one. Above 2 mph GPS speed the dish locks forward (transit mode, forward returns still live); below it, full sweeps resume, alternating direction each pass so the dish never wastes time slewing home. Elevation is manual on this dome, and the offset-dish geometry couples elevation into azimuth — the startup prompt asks for your elevation and applies a 0.4°-per-degree azimuth correction to every return.

Browser PPI radar scope showing two storm cells, alert feed, and a projected track cone
The live scope: PPI sweep, county/road overlay, tiered alert feed, and the widening 20-minute track cone. Demo at radar.koakno.com.

The FCC problem nobody in DIY-radar land talks about

Receive-only operation needs no license; transmitting on 10.0–10.5 GHz needs at least a US Technician class — and, less obviously, the emission type has to be authorized (rules differ outside the US). That's the landmine: 47 CFR 97.305(c)(6)(ii) authorizes MCW, phone, image, RTTY, data, SS, and test on the 3 cm band — plain pulse is not on the list, though the neighboring 5 cm and 1.2 cm bands explicitly allow it. So the original pulsed design isn't something to just key up. The repo floats two outs, committed to neither: the FMCW chirp plausibly reading as SS/data (both authorized), and a pulsed scheme where each pulse is the Morse-keyed callsign, framed as a §97.203 propagation beacon studying backscatter. The README then does something unusual — it reports that outside review pushed back hard on the beacon theory (a Morse wrapper doesn't change a radiolocation purpose) and declines to present it as settled. Meanwhile sdr.py already implements the 47 CFR 97.119 station ID: callsign in CW every ≤10 minutes at 18 WPM (the 20 WPM legal ceiling is noted in config.py), with the Pluto pausing its chirp to key the ID. Until the emission question resolves, the sanctioned paths are receive-only — an RTL-SDR V4 on the SEC port, whose built-in bias tee also powers the LNB, ~$35 — or pure simulation.

Build notes & gotchas

Essential BOM is roughly $50 plus the SDR you already own: dome from salvage ($5–40), 2× F-to-SMA adapters, RS-485 converter, 6-conductor RJ-25 cord, USB-serial, and T10/T15 Torx for the inner dome. A 10 dB SMA attenuator on the TX path is recommended armor against close-range reflections.

💎 Buried detail: clone-and-run fails. The repo root is a flat pile of files, but app.py calls Flask's render_template('index.html'), which requires a templates/ directory — and the geojson overlays must live in static/. The properly-arranged tree only exists inside portable_radar.zip in the repo. Unzip that (or rebuild the layout by hand), run simplify_maps.py once, then python3 run.py.
sudo apt install python3-pip python3-numpy soapysdr-tools soapysdr-module-hackrf
pip install flask pyserial          # + pyadi-iio for the Pluto path
python3 simplify_maps.py            # one-time geojson optimization
python3 run.py                      # scope at http://localhost:5000

It also runs under Termux on Android (confirmed by the author, with everything falling back to sim/manual mode gracefully). An August community-testing round fixed a solid list of real bugs — the 0°/360° double-sample, a DANGER threshold that didn't match its own wording, GPS fix status that never cleared, missing NMEA checksum validation, and a range scale hardcoded in five display locations — which is more field-hardening than most weekend radar projects ever get.

Glossary

Bias tee
A passive network that puts DC power onto a coax cable without disturbing the RF riding on it — here, how the LNB gets powered through the SEC line.
Dechirp (stretch processing)
Multiplying a received FMCW echo by the conjugate of the transmitted chirp so each target's delay becomes a constant beat frequency an FFT can read as range.
FMCW (Frequency-Modulated Continuous Wave)
Radar that transmits nonstop while sweeping its frequency, measuring range from the frequency difference between what's going out and what's coming back.
LNB (Low-Noise Block downconverter)
The feed unit at a satellite dish's focus that amplifies the microwave signal and mixes it down to a lower IF that cheap coax and tuners can handle.
NEXRAD (Next-Generation Weather Radar)
The NWS network of ~160 fixed S-band Doppler radars behind almost every US radar app; volume scans take minutes, hence the data latency.
PPI (Plan Position Indicator)
The classic top-down circular radar display — the radar at center, echoes painted by bearing and range as the sweep rotates.
SDR (Software-Defined Radio)
A radio whose modulation and signal processing happen in software, so one board can be a receiver, transmitter, or — as here — a radar core.
X-band
The 8–12 GHz radar band; its short wavelength scatters well off raindrops, which is why marine and mobile weather radars live here.

Sources & References

Comments