1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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.
|