summaryrefslogtreecommitdiff
path: root/radarFetch.sh
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 /radarFetch.sh
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 'radarFetch.sh')
-rwxr-xr-xradarFetch.sh190
1 files changed, 190 insertions, 0 deletions
diff --git a/radarFetch.sh b/radarFetch.sh
new file mode 100755
index 0000000..0dcc457
--- /dev/null
+++ b/radarFetch.sh
@@ -0,0 +1,190 @@
+#!/bin/bash
+set -euo pipefail
+shopt -s nullglob
+
+# SJP 2026-06-09
+# Fetch BOM radar layers via anonymous FTP, alpha-composite them,
+# publish the last FRAME_COUNT frames as an APNG loop.
+# Parameterised on product code; default IDR713 (Sydney 128 km).
+# Deployed to /opt/radar/radarFetch.sh on cremonde for radar.pestrel.com.
+
+RADAR_ID="${1:-IDR713}"
+FRAME_COUNT="${FRAME_COUNT:-6}"
+FRAME_DELAY="${FRAME_DELAY:-50}" # centiseconds per frame
+END_PAUSE="${END_PAUSE:-150}" # centiseconds, pause on last frame
+TRANSPARENCY_TTL_HOURS="${TRANSPARENCY_TTL_HOURS:-24}"
+
+DATA_ROOT="/var/lib/radar/${RADAR_ID,,}"
+TRANS_DIR="${DATA_ROOT}/transparencies"
+FRAMES_DIR="${DATA_ROOT}/frames"
+PLATES_DIR="${DATA_ROOT}/plates"
+OUT_DIR="${DATA_ROOT}/out"
+PUBLISH_DIR="/srv/www/radar"
+PUBLISH_PATH="${PUBLISH_DIR}/${RADAR_ID,,}-loop.apng"
+
+FTP_DYNAMIC="ftp://ftp.bom.gov.au/anon/gen/radar/"
+FTP_STATIC="ftp://ftp.bom.gov.au/anon/gen/radar_transparencies/"
+
+require_cmd() {
+ command -v "$1" >/dev/null 2>&1 || { echo "Required command not found: $1" >&2; exit 127; }
+}
+require_cmd curl
+require_cmd magick
+
+mkdir -p "$TRANS_DIR" "$FRAMES_DIR" "$PLATES_DIR" "$OUT_DIR"
+
+fetch_validated() {
+ # $1 url $2 dest
+ local url="$1" dest="$2" tmp="${2}.tmp"
+ curl -q -fs -o "$tmp" "$url" || { rm -f "$tmp"; return 1; }
+ if magick identify "$tmp" >/dev/null 2>&1; then
+ mv "$tmp" "$dest"
+ else
+ rm -f "$tmp"
+ return 1
+ fi
+}
+
+# --- 1. Refresh transparencies if stale ----------------------------------
+TRANS_MARKER="${TRANS_DIR}/.last_refreshed"
+needs_refresh=1
+if [ -f "$TRANS_MARKER" ]; then
+ age_h=$(( ( $(date +%s) - $(stat -c %Y "$TRANS_MARKER") ) / 3600 ))
+ [ "$age_h" -lt "$TRANSPARENCY_TTL_HOURS" ] && needs_refresh=0
+fi
+
+if [ "$needs_refresh" -eq 1 ]; then
+ echo "Refreshing transparencies for ${RADAR_ID}..."
+ if listing=$(curl -q -fs "$FTP_STATIC"); then
+ # ls -l style listing; filename is the last whitespace-delimited field
+ while read -r fname; do
+ [ -n "$fname" ] || continue
+ fetch_validated "${FTP_STATIC}${fname}" "${TRANS_DIR}/${fname}" \
+ || echo " failed: ${fname}" >&2
+ done < <(echo "$listing" | awk '{print $NF}' | grep "^${RADAR_ID}\." || true)
+ touch "$TRANS_MARKER"
+ # invalidate precomposited plates
+ rm -f "${PLATES_DIR}/lower.png" "${PLATES_DIR}/upper.png"
+ else
+ echo "Transparency listing failed; keeping cached copies." >&2
+ fi
+fi
+
+# --- 2. Build precomposited plates ---------------------------------------
+BACKGROUND="${TRANS_DIR}/${RADAR_ID}.background.png"
+if [ ! -f "$BACKGROUND" ]; then
+ echo "No background transparency cached; cannot composite." >&2
+ exit 1
+fi
+
+existing_layers() {
+ # echo paths that exist, in given order
+ local base p
+ for base in "$@"; do
+ p="${TRANS_DIR}/${RADAR_ID}.${base}.png"
+ [ -f "$p" ] && echo "$p"
+ done
+}
+
+if [ ! -f "${PLATES_DIR}/lower.png" ]; then
+ # background (opaque) + topography + optional feature overlays under the echo
+ readarray -t lower_layers < <(existing_layers \
+ background topography catchments waterways wthrDistricts roads rail)
+ magick "${lower_layers[@]}" -background none -layers flatten "${PLATES_DIR}/lower.png"
+fi
+
+if [ ! -f "${PLATES_DIR}/upper.png" ]; then
+ # range rings + place labels go over the echo so they stay legible
+ readarray -t upper_layers < <(existing_layers range locations)
+ if [ "${#upper_layers[@]}" -gt 0 ]; then
+ magick "${upper_layers[@]}" -background none -layers flatten "${PLATES_DIR}/upper.png"
+ fi
+fi
+
+# --- 3. Determine current top-N frames on the FTP ------------------------
+echo "Listing dynamic frames..."
+if ! listing=$(curl -q -fs "$FTP_DYNAMIC"); then
+ echo "Dynamic listing failed; serving last good loop." >&2
+ exit 0
+fi
+
+mapfile -t remote_frames < <(echo "$listing" \
+ | awk '{print $NF}' \
+ | grep "^${RADAR_ID}\.T\.[0-9]\{12\}\.png$" \
+ | sort \
+ | tail -n "$FRAME_COUNT")
+
+if [ "${#remote_frames[@]}" -eq 0 ]; then
+ echo "No remote frames for ${RADAR_ID}; serving last good loop." >&2
+ exit 0
+fi
+
+# --- 4. Fetch missing frames; evict frames outside the rolling buffer ----
+for fname in "${remote_frames[@]}"; do
+ if [ ! -f "${FRAMES_DIR}/${fname}" ]; then
+ echo " fetching ${fname}"
+ fetch_validated "${FTP_DYNAMIC}${fname}" "${FRAMES_DIR}/${fname}" \
+ || echo " failed: ${fname}" >&2
+ fi
+done
+
+keep_set=" $(printf '%s ' "${remote_frames[@]}")"
+for existing in "$FRAMES_DIR"/${RADAR_ID}.T.*.png; do
+ bn=$(basename "$existing")
+ case "$keep_set" in
+ *" $bn "*) ;;
+ *) rm -f "$existing" ;;
+ esac
+done
+
+# --- 5. Composite each held frame ----------------------------------------
+LEGEND="${TRANS_DIR}/${RADAR_ID}.legend.0.png"
+HAVE_UPPER=0; [ -f "${PLATES_DIR}/upper.png" ] && HAVE_UPPER=1
+HAVE_LEGEND=0; [ -f "$LEGEND" ] && HAVE_LEGEND=1
+
+# clear stale composited frames
+rm -f "${OUT_DIR}"/frame.*.png
+
+composited=()
+seq=0
+for fname in "${remote_frames[@]}"; do
+ src="${FRAMES_DIR}/${fname}"
+ [ -f "$src" ] || continue
+ out="${OUT_DIR}/frame.$(printf '%02d' "$seq").png"
+
+ cmd=(magick "${PLATES_DIR}/lower.png" "$src" -composite)
+ [ "$HAVE_UPPER" -eq 1 ] && cmd+=("${PLATES_DIR}/upper.png" -composite)
+ [ "$HAVE_LEGEND" -eq 1 ] && cmd+=(-gravity southeast "$LEGEND" -composite +gravity)
+ cmd+=("$out")
+ "${cmd[@]}"
+
+ composited+=("$out")
+ seq=$((seq + 1))
+done
+
+if [ "${#composited[@]}" -eq 0 ]; then
+ echo "No frames composited; serving last good loop." >&2
+ exit 0
+fi
+
+# --- 6. Assemble APNG loop -----------------------------------------------
+# Per-frame delays: last frame gets END_PAUSE so the eye can rest on "now".
+mkdir -p "$PUBLISH_DIR"
+TMP_APNG="${OUT_DIR}/loop.apng.tmp"
+
+build_cmd=(magick -loop 0)
+last_idx=$(( ${#composited[@]} - 1 ))
+for i in "${!composited[@]}"; do
+ if [ "$i" -eq "$last_idx" ]; then
+ build_cmd+=(-delay "$END_PAUSE" "${composited[$i]}")
+ else
+ build_cmd+=(-delay "$FRAME_DELAY" "${composited[$i]}")
+ fi
+done
+build_cmd+=("$TMP_APNG")
+"${build_cmd[@]}"
+
+install -m 644 "$TMP_APNG" "$PUBLISH_PATH"
+rm -f "$TMP_APNG"
+
+echo "Published ${PUBLISH_PATH} (frames=${#composited[@]}, latest=${remote_frames[-1]})"