First step toward physics-based HF / sporadic-E scoring. Polls GIRO's DIDBase tabular endpoint every 10 minutes for two US ionosondes (Millstone Hill MHJ45, Alpena AL945), parses the plain-text response into observation rows, and upserts into a new ionosonde_observations table keyed on (station_code, valid_time). This gets foEs (E-layer sporadic critical frequency) into the database as a direct measurement — the key input for predicting 144 MHz Es openings via ITU-R P.534-6. foF2 + MUFD are the F2-layer inputs for HF MUF predictions. Not yet wired into scoring. Boulder/Wallops/Austin/Idaho/Point Arguello are in the GIRO catalog but were silent when probed — add them back if/when they come online. Next steps: SWPC JSON (Kp, F10.7, sunspot), GOES X-ray flux, D-RAP text, and the P.534-6 Es scoring factor that uses foEs at midpoint for the 144/440 band configs.
62 lines
2 KiB
Elixir
62 lines
2 KiB
Elixir
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.Worker,
|
|
queue: :ionosphere,
|
|
max_attempts: 3,
|
|
unique: [period: 300, states: [:available, :scheduled, :executing, :retryable]]
|
|
|
|
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.Worker
|
|
def perform(%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: #{inspect(reason)}")
|
|
end
|
|
end
|
|
end
|