defmodule Microwaveprop.Workers.IonosphereFetchWorker do @moduledoc """ Polls GIRO DIDBase for real-time ionosonde measurements (foF2, foE, foEs, hmF2, MUFD) from a handful of CONUS stations. Drives the 144 MHz sporadic-E and HF MUF scoring inputs. Runs on a ~10-minute cron — GIRO publishes each station at 7.5-minute cadence so a 10-minute poll guarantees we pick up every new sample within one cycle without hammering the endpoint. Pulls a 2-hour window each poll to catch any late-arriving samples and to be resilient to the occasional missed run. Upserts are idempotent on (station_code, valid_time). """ use Oban.Pro.Worker, queue: :ionosphere, max_attempts: 3, unique: [period: 300, states: :incomplete] alias Microwaveprop.Ionosphere alias Microwaveprop.Ionosphere.GiroClient require Logger # CONUS stations that have been publishing reliably. Others (Boulder, # Wallops, Austin, Idaho, Point Arguello) are in the GIRO catalog but # intermittent — add them here once they come back online. @stations [ %{code: "MHJ45", name: "Millstone Hill MA", lat: 42.6, lon: -71.5}, %{code: "AL945", name: "Alpena MI", lat: 45.1, lon: -83.6} ] @lookback_seconds 2 * 3600 @impl Oban.Pro.Worker def process(%Oban.Job{}) do now = DateTime.truncate(DateTime.utc_now(), :second) from_dt = DateTime.add(now, -@lookback_seconds, :second) Enum.each(@stations, fn station -> fetch_and_upsert(station, from_dt, now) end) :ok end @doc false @spec stations() :: [map()] def stations, do: @stations defp fetch_and_upsert(%{code: code, name: name}, from_dt, to_dt) do case GiroClient.fetch(code, from_dt, to_dt) do {:ok, observations} -> {:ok, count} = Ionosphere.upsert_observations(code, observations) Logger.info("IonosphereFetch: #{code} (#{name}) upserted #{count} observations") {:error, reason} -> Logger.warning("IonosphereFetch: #{code} (#{name}) failed: #{format_reason(reason)}") end end # GIRO's TLS cert chains through a CA that isn't always in the pod's # bundle, surfacing as a Req.TransportError with a multi-line erl_alert # string. Inspecting the full struct dumps ~400 chars of stacktrace-y # noise per station per 10-min cycle. Compress to something grep-able. defp format_reason("GIRO request failed: " <> tail) do cond do String.contains?(tail, ":unknown_ca") -> "TLS unknown_ca (CA bundle missing)" String.contains?(tail, ":timeout") -> "timeout" String.contains?(tail, ":econnrefused") -> "connection refused" true -> String.slice(tail, 0, 120) end end defp format_reason(reason) when is_binary(reason), do: reason defp format_reason(reason), do: inspect(reason, printable_limit: 200) end