summaryrefslogtreecommitdiff
path: root/doc/bom-radar-spec.md
diff options
context:
space:
mode:
authorSt33v <github@f3rr3t.com>2026-06-09 13:04:04 +1000
committerSt33v <github@f3rr3t.com>2026-06-09 13:04:04 +1000
commitf058e83da43b0b661b45a7bd4c82b49c57e61d93 (patch)
treeeffc21ef57b00b45c8caa29424772f7fbbde03d8 /doc/bom-radar-spec.md
parentc132b3985e24e557549544a1d10fb0daababdfb1 (diff)
Add IDR713 radar fetch and APNG loop
radarFetch.sh fetches BOM IDR713 transparencies (24h cache) and the last six 6-minute echo frames, composites them via cached lower/upper plates, and publishes an APNG loop to /srv/www/radar/. systemd: radar.{service,timer} on a 6-min cadence, with retry units mirroring the synoptic pattern. nginx: new radar.pestrel.com vhost (still commented in setup.sh until DNS propagation is confirmed). setup.sh provisions radar dirs, installs radar units, enables timer. deploy.sh accepts synoptic|radar arg. Parameterised on product code; adding another radar is a one-line config change. Spec: doc/bom-radar-spec.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diffstat (limited to 'doc/bom-radar-spec.md')
-rw-r--r--doc/bom-radar-spec.md249
1 files changed, 249 insertions, 0 deletions
diff --git a/doc/bom-radar-spec.md b/doc/bom-radar-spec.md
new file mode 100644
index 0000000..3123a16
--- /dev/null
+++ b/doc/bom-radar-spec.md
@@ -0,0 +1,249 @@
+---
+title: "BOM Radar Display — Sydney (IDR713)"
+date-created: "2026-06-09"
+version: "0.1"
+type: "spec"
+author:
+ - "st33v"
+ - "claude-opus-4-8"
+model:
+ - "claude-opus-4-8"
+tldr: "Extend the existing bomsynoptic app to fetch, composite and serve Sydney radar (IDR713) from BOM anonymous FTP. Layer model: static transparencies (cached) plus a rolling buffer of the last six 6-minute rain-echo frames, served as a loopable sequence alongside the synoptic chart."
+status: "draft"
+spec-kind: "feature"
+target-repo: "bomsynoptic"
+target-machine: "cremonde"
+dependencies: []
+design-chat: "https://claude.ai/chat/..."
+phase: "1"
+phases-planned: 1
+implementing-agent: "claude-code"
+reader-targets:
+ - "claude-code"
+ - "st33v"
+tags:
+ - "bom"
+ - "radar"
+ - "ftp"
+---
+
+# BOM Radar Display — Sydney (IDR713)
+
+## 1. Purpose
+
+The `bomsynoptic` app currently fetches and serves the BOM MSLP synoptic
+chart on a six-hourly cadence. This spec adds a second product class:
+**rain radar**, beginning with the **128 km Sydney (Terrey Hills)** radar,
+product code **`IDR713`**.
+
+Unlike the synoptic chart — a single self-contained image — a radar view is a
+*composite* of several PNG layers that must be stacked in the correct order,
+and the rain-echo layer refreshes roughly every six minutes. The deliverable
+is the current radar image plus a short loop of the last half-dozen frames,
+served in the same manner as the existing synoptic chart.
+
+Brisbane (Mt Stapylton) and other radars are explicitly **out of scope** for
+this phase; the design must make duplication to a new product code trivial
+(§9).
+
+## 2. How BOM radar works (background for the implementer)
+
+BOM does not publish a single ready-made radar image. It publishes a set of
+**transparent PNG layers**, all sharing identical pixel dimensions, intended
+to be alpha-composited in a fixed order. Two distinct cadences apply:
+
+- **Static layers** (basemap, topography, range rings, place labels, feature
+ overlays, legend) change only rarely — when BOM revises the basemap. Fetch
+ once, cache, refresh occasionally (e.g. daily).
+- **Dynamic layers** (the rain-echo frames) are emitted **every ~6 minutes**.
+ Each frame is a separate timestamped file. Several recent frames are present
+ on the server at any moment; we take the most recent *N*.
+
+All files live under BOM's **anonymous FTP** service. Availability is
+best-effort — BOM explicitly does not guarantee it — so resilience matters
+(§8).
+
+## 3. FTP source
+
+Anonymous FTP, no credentials beyond user `anonymous` (empty / any password).
+
+| Layer class | Directory |
+|---|---|
+| Rain-echo frames (dynamic) | `ftp://ftp.bom.gov.au/anon/gen/radar/` |
+| Transparencies (static) | `ftp://ftp.bom.gov.au/anon/gen/radar_transparencies/` |
+
+### 3.1 Dynamic frames
+
+Filename pattern in `/anon/gen/radar/`:
+
+```
+IDR713.T.<YYYYMMDDHHMM>.png
+```
+
+- The timestamp is **UTC**.
+- Because the timestamp is fixed-width and big-endian, a plain lexical sort of
+ the matching filenames is also a chronological sort.
+- Selection: list the directory, filter to the `IDR713.T.` prefix, sort, take
+ the last **6** (configurable as `FRAME_COUNT`, default 6).
+
+### 3.2 Static transparencies
+
+Files in `/anon/gen/radar_transparencies/`, all prefixed `IDR713.`:
+
+| File | Role |
+|---|---|
+| `IDR713.background.png` | base map: land/sea, coastline |
+| `IDR713.topography.png` | terrain shading |
+| `IDR713.range.png` | range rings |
+| `IDR713.locations.png` | town / city labels |
+| `IDR713.roads.png` | roads (optional overlay) |
+| `IDR713.rail.png` | rail (optional overlay) |
+| `IDR713.waterways.png` | rivers (optional overlay) |
+| `IDR713.catchments.png` | catchment boundaries (optional) |
+| `IDR713.wthrDistricts.png`| weather district boundaries (optional) |
+| `IDR713.legend.0.png` | colour-scale legend |
+
+Not every transparency exists for every radar. The implementer should fetch
+the directory listing once and treat any missing optional layer as absent
+rather than failing.
+
+## 4. Compositing model
+
+Alpha-composite **bottom → top** in this order. Optional feature overlays sit
+between topography and the radar echoes or above them per taste; the
+constraint that matters is that **labels and range rings render on top of the
+rain** so they stay legible.
+
+```
+1. IDR713.background.png ← bottom (opaque base)
+2. IDR713.topography.png
+3. (optional) catchments / waterways / roads / rail / wthrDistricts
+4. IDR713.T.<timestamp>.png ← the rain echoes (per frame)
+5. IDR713.range.png
+6. IDR713.locations.png ← top (labels)
+```
+
+The **legend** (`IDR713.legend.0.png`) is a separate strip. Either composite
+it into a fixed corner/margin of the output, or serve it as a sibling asset
+the front-end positions beside the loop. Recommend compositing it in so the
+served image is self-describing.
+
+Implementation note: build the **static base** (layers 1–3 + the on-top
+overlays 5–6 are split around the echo, so cache them as two precomposited
+plates — a *lower plate* under the echo and an *upper plate* over it). For
+each frame: `lower_plate → echo → upper_plate`. This reduces per-frame work to
+two composites instead of seven.
+
+Suggested tooling: Pillow (`PIL.Image.alpha_composite`) keeps the dependency
+footprint small and matches the likely existing stack; ImageMagick is an
+acceptable alternative if already present.
+
+## 5. Fetch cadence & rolling buffer
+
+- **Echo frames:** poll every **6 minutes** (`POLL_INTERVAL`, default 360 s).
+ On each poll, fetch only frames not already held locally (compare against
+ the buffer). Maintain a rolling buffer of the last `FRAME_COUNT` (6) frames;
+ evict older raw frames and their composites.
+- **Transparencies:** refresh on startup and then daily (`TRANSPARENCY_TTL`,
+ default 24 h). Survive a failed refresh by keeping the cached plates.
+- Do not hammer the server: the 6-minute poll matches BOM's emission rate.
+ Skip a poll cleanly if the previous one is still running.
+
+Note the contrast with the existing synoptic job: synoptic is six-**hourly**;
+radar is six-**minute**. The scheduler must accommodate both — reuse the
+existing scheduling mechanism if it generalises, otherwise run radar on its
+own timer.
+
+## 6. Outputs
+
+Produce two things per cycle:
+
+1. **Latest still** — the most recent frame composited over the plates.
+ Stable filename (e.g. `idr713-latest.png`) so the served URL never
+ changes.
+2. **Loop** — the last 6 composited frames. Serve either as:
+ - an **animated APNG/GIF** (`idr713-loop.png` / `.gif`) — simplest for the
+ front-end, single asset, frame timing baked in; or
+ - a **numbered sequence** (`idr713.0.png … idr713.5.png`) plus a small JSON
+ manifest of frame timestamps, leaving animation to client-side JS.
+
+Recommend the **sequence + manifest** route: it mirrors how BOM's own viewer
+works, lets the front-end control frame rate and pausing, and avoids
+re-encoding a whole animation every six minutes. The manifest also lets the UI
+print the real observation time (converted from UTC to the user's / Sydney
+local time) under each frame.
+
+## 7. Storage layout (suggested)
+
+```
+<data-root>/radar/idr713/
+ transparencies/ # cached static PNGs, refreshed daily
+ frames/ # raw echo PNGs, rolling buffer of 6
+ out/
+ idr713-latest.png
+ idr713.0.png … idr713.5.png
+ manifest.json # [{seq, utc, local, src_filename}, …]
+```
+
+Mirror whatever convention the synoptic side already uses for its served
+directory; the above is a fallback if there's no established pattern.
+
+## 8. Resilience
+
+- **Missing / partial frames:** if a poll returns fewer than 6 frames, serve
+ what exists; never blank the display.
+- **FTP unreachable:** retain and keep serving the last good composites; log
+ and retry next cycle. BOM does not guarantee availability.
+- **Stale data:** stamp the manifest with the newest frame's UTC time so the
+ UI can grey out / warn if the latest echo is older than, say, 20 minutes.
+- **Corrupt download:** validate each PNG opens before it enters the buffer;
+ discard and retry otherwise.
+- **Dimension mismatch:** if a transparency and the echo frames disagree on
+ size (can happen across a BOM basemap revision), refetch transparencies
+ before compositing.
+
+## 9. Generalising to other radars
+
+Parameterise on the product code. A single config entry should be enough to
+add Brisbane (Mt Stapylton) later:
+
+```yaml
+radars:
+ - id: "IDR713" # Sydney (Terrey Hills) 128 km
+ enabled: true
+ - id: "IDR663" # Brisbane (Mt Stapylton) 128 km — confirm code before enabling
+ enabled: false
+```
+
+Everything else — directories, filename patterns, layer set, compositing
+order — is identical across radars, differing only by the `IDRnnn` stem.
+**Do not hardcode `IDR713`.** (The Brisbane code above is a placeholder;
+st33v will confirm the exact product before that radar is switched on.)
+
+## 10. Copyright / terms of use
+
+BOM anonymous-FTP products are licensed for personal or in-organisation use,
+**not** commercial use, and republishing is meant to go through Registered
+User services. Since `st33v.com` is a public-facing surface, this is worth a
+deliberate decision rather than an oversight. Options: attribute BOM clearly
+and treat the personal/non-commercial display as within bounds, gate the radar
+behind the internal (WireGuard) surface only, or register. **Flag for st33v —
+do not silently publish.**
+
+## 11. Out of scope (this phase)
+
+- Brisbane and all radars other than IDR713 (config-ready, disabled).
+- Doppler wind / velocity products and the rainfall-accumulation products.
+- Historical / archived frames beyond the rolling 6-frame buffer.
+- Any front-end beyond serving the still, the sequence, and the manifest in
+ the same idiom as the existing synoptic chart.
+
+## 12. Open questions for Claude Code
+
+1. Does the existing scheduler generalise to a 6-minute timer, or does radar
+ need its own?
+2. What's the established served-assets directory and URL pattern on the
+ synoptic side — reuse it.
+3. Pillow already a dependency, or introduce it?
+4. Loop-as-sequence (recommended) vs animated single asset — confirm against
+ the existing front-end's capabilities.