Active Oct 2026

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.

EndpointMedian connect + requestCeiling at a 0.25 s poll
localhost304.4 ms1.8 Hz
127.0.0.10.75 ms4.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 localhost stall 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 --test cases, and a GitHub Actions job on Windows
  • Ops: PowerShell launchers with preflight checks, a retention script that only previews unless given -Apply