GOES-19 Ground Station (Home Weather Satellite Receiver & WeatherStar-Style Display)
Role: Solo, end-to-end: receiver setup, monitoring and recovery tools, weather data service, display integration, and the dish
Tech Stack: SatDump, RTL-SDR, Python 3.12 (stdlib supervisor and dashboard server), Node 20 display apps, pytest and node --test, PowerShell launchers
Repository: Private
Evidence: Dish siting tool, rebuilt for a public site
Overview
GOES-19 is the NOAA weather satellite parked over the eastern US. It also broadcasts a stream anyone with a small dish can receive: HRIT imagery, EMWIN text and graphics (forecasts, warnings, discussions), and DCS observations relayed from ground stations. This project receives that stream at home and turns it into a weather display that runs on its own.
The chain is a dish, an LNA and an RTL-SDR feeding SatDump, which demodulates the downlink and writes products to disk. A service I call Weather Core ingests, parses and caches those products behind one internal weather model, and two display apps read from it: a WeatherStar 4000 style Classic mode (a fork of ws4kp) and a Pi Weather Station style Modern mode (a fork of pi-weather-station), plus an Auto mode that cycles both. The goal in the spec is a display that āshould behave more like a dedicated weather appliance than a conventional web dashboardā: it starts on power-up, needs no routine interaction, and keeps showing cached data if the receiver drops out.
Engineering Details
1. Keeping the Receiver Alive
SatDump runs continuously, and sometimes its status freezes: the process is up, but SNR, frequency, bit error rate and both lock flags stop changing. A supervisor wraps it. Every 5 seconds it polls SatDumpās local status API, and if all five values sit unchanged for 90 seconds (after a 2-minute startup grace), it sends the same console break Ctrl+C would and waits for SatDump to finish saving partial products before starting a new process.
It never force-kills SatDump during that flush. A stuck shutdown shows up in the receiverās terminal instead of being traded for incomplete output. The dashboard and signal logger are deliberately not restarted with it and reconnect on their next poll.
2. 304 Milliseconds of IPv6
The dashboard polled SatDumpās API through localhost. Measured on 2026-09-20 against the live receiver: SatDump binds IPv4 only, and on this machine localhost resolves to IPv6 first, so every request tried IPv6, waited for the timeout, and fell back.
| Endpoint | Median connect + request | Ceiling at a 0.25 s poll |
|---|---|---|
localhost | 304.4 ms | 1.8 Hz |
127.0.0.1 | 0.75 ms | 4.0 Hz |
The predicted 1.8 Hz ceiling matched the 1.78 rows per second the CSV had actually been logging. After switching every endpoint to a literal 127.0.0.1, the dashboardās proxy answers in 1.6 ms.
3. Aiming the Dish
The dish is the hard part, and alignment is still in progress. The first capture window I inventoried (2026-09-20) was taken with the dish known to be poorly aligned, and it shows. EMWIN came through: 2,640 text, radar and graphics files in under four hours. But the best full-disk image from that window decodes with large black missing areas and horizontal gaps, no DCS messages arrived at all, and a two-day signal log (09-19 to 09-21) tops out just under 6 dB SNR with frequent lock and unlock events.
First I needed to know whether the dish could see the satellite at all. GOES-19 sits at a fixed point in the sky, so I built a siting tool on top of the horizon and canopy code from dark-sky and the lidar work from TALON. It precomputes 81 candidate spots on a 5 m grid. For each one it traces terrain 100 km toward the satellite and checks a leaf-off lidar scan for trees and buildings in the first 170 m. Sliders cover dish height, aim error, and tree growth, and the verdict wants 2 degrees of clearance. The shareable version runs the same check at Mount Mitchell State Park instead of my yard.
It confirmed what I already suspected about where the dish could go, but it was nice to see that without going outside with a level and a compass.
For peaking the signal, I wanted a spectrum waterfall on the dashboard. Measured against SatDump v1.2.2, the command-line receiver canāt provide one: every spectrum path on its HTTP server returns āPlease use /api for JSON data,ā and 15 hours of CLI operation logged no FFT setup at all, while the GUI logs one within seconds of launch.
So I built an alignment mode instead: one large SNR number and bar readable at armās length, a resettable peak, a 15-second sparkline, and a tone that rises with signal. Reset the peak, make one adjustment, and see if the new peak beats the old one. Using it outside taught me a few things:
- SatDumpās own peak SNR is useless for aiming. Itās a lifetime high-water mark that never resets, so it canāt tell you whether the last nudge helped. The alignment view keeps its own peak.
- The dashboard had to work as a phone instrument. Iām at the dish, not the desk, so it got a sticky header and bigger tap targets.
- Browsers fight you outdoors. Screen Wake Lock needs a secure context, and plain http to a LAN address isnāt one, so the phoneās screen can sleep mid-adjustment until the dashboard is served over https. The tone wonāt start until you tap a button, because browsers block audio until the user interacts.
- The scale is a guess. The bar fills at 10 dB, a placeholder taken from the guideās āabove 3 dB is cleanā thresholds. It gets replaced once I know the best SNR this dish actually reaches.
Current State and Limits
- Displays: the radar screen is still showing a satellite imagery fixture instead of real radar, and fresh captures are missing. Some browser tests hang in one test environment before producing a verdict.
- Reception: dish alignment is ongoing, and a better-aligned capture window still needs its own inventory. DCS reception, the local forecast, and stable local radar arenāt confirmed yet.
- Security: the projectās security audit found nothing critical or high. Two LAN exposures are documented as acceptable on a trusted home network.
Build Log
The receiver was already running and logging on 2026-09-19. The repo started the next day, and all 803 of my commits landed across four days.
- 09-20 (242 commits): Receiver scripts, signal monitor and dashboard brought into the repo. Classic and Modern upstream apps imported. Weather Coreās schema and persistence started. The
localhoststall measured and alignment mode built. - 09-21 (209 commits): More Weather Core work, and the receiver supervisor with graceful restart. GitHub stops at that supervisor commit.
- 09-22 to 09-23 (352 commits): Display screens, alerts and Weather Core parsers, with the gaps listed above. Local only so far.
Code Snippet: Knowing When the Receiver Is Frozen
@dataclass
class ReceiverStatusWatch:
"""Identify a complete SatDump status response that has stopped changing."""
stale_after_seconds: float
signature: tuple[object, ...] | None = None
changed_at: float | None = None
def __post_init__(self) -> None:
if self.stale_after_seconds <= 0:
raise ValueError("stale_after_seconds must be positive")
@staticmethod
def fingerprint(payload: dict[str, Any]) -> tuple[object, ...] | None:
values: list[object] = []
for section, field in STATUS_FIELDS:
source = payload.get(section)
if not isinstance(source, dict) or source.get(field) is None:
return None
values.append(source[field])
return tuple(values)
def observe(self, payload: dict[str, Any], now: float) -> bool:
current = self.fingerprint(payload)
if current is None:
self.signature = None
self.changed_at = None
return False
if current != self.signature:
self.signature = current
self.changed_at = now
return False
return self.changed_at is not None and now - self.changed_at >= self.stale_after_seconds
A partial or missing status response resets the clock instead of counting toward a restart, so a receiver thatās slow to answer is never mistaken for a frozen one.
Tech Stack
- Radio: RTL-SDR, SatDump v1.2.2 (HRIT demodulation and product decoding)
- Receiver tools: Python 3.12 standard library for the supervisor, terminal monitor and CSV logger, and a dashboard server that proxies SatDumpās API so the browser page needs no internet access
- Weather Core: Python ingestion, parsing and caching for EMWIN, DCS and imagery products, with a Weather.gov-compatible layer for the displays
- Displays: ws4kp and pi-weather-station forks (both MIT), Node 20
- Testing: 121 pytest tests and about 49
node --testcases, and a GitHub Actions job on Windows - Ops: PowerShell launchers with preflight checks, a retention script that only previews unless given
-Apply