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