Subscribes to mqtt.pskreporter.info:1883 over plain TCP and folds every reception report into hourly aggregates per (band, sender_grid_4, receiver_grid_4). Each spot is the empirical "propagation actually occurred on this path right now" signal we'll correlate against HRRR atmospheric state to recalibrate the scoring weights. Aggregating at the path-hour bucket collapses 50-reporter QSOs to one row instead of 50. Topic filter anchors on USA (DXCC 291) on either sender or receiver side so the broker pre-filters out everything outside our HRRR domain. Bands subscribed: VHF and up (6m, 2m, 70cm, 23cm, + microwave) — HF is dominated by ionospheric propagation, which this project doesn't model. Pskr.Client cluster-elects a singleton via :global.register_name — all replicas start the GenServer but only one connects to MQTT; the rest stay in :standby and watch for the leader's nodedown to re-run election. Off in dev/test, on by default in prod (PSKR_MQTT_ENABLED=false as kill-switch). Pskr.Aggregator buffers in memory keyed by Pskr.path_key/1 and flushes every 60s via Repo.insert_all/3 with an additive ON CONFLICT (count summed, SNR envelope by GREATEST/LEAST, modes unioned via unnest+DISTINCT). Idempotent across overlapping flushes and across leader handover. Dockerfile sets BUILD_WITHOUT_QUIC=1 — emqtt's transitive quicer dep wants CMake + OpenSSL headers we'd otherwise have to add to the builder image just to never use QUIC over plain MQTT. Base image is unchanged; the new dep compiles cleanly into the existing prop-base runtime.
68 lines
2.4 KiB
Elixir
68 lines
2.4 KiB
Elixir
defmodule Microwaveprop.Pskr.SpotHourly do
|
|
@moduledoc """
|
|
Hourly aggregate of PSK Reporter spots between two 4-character
|
|
Maidenhead grid squares on a single band.
|
|
|
|
A single QSO often produces 50+ reception reports (one per
|
|
monitoring station that decoded it). Storing each report verbatim
|
|
would dwarf the rest of the database — instead `Pskr.Aggregator`
|
|
collapses every spot inside `(hour_utc, band, sender_grid,
|
|
receiver_grid)` into one row, retaining `spot_count`, SNR
|
|
envelope, and the set of modes observed. Downstream weather
|
|
correlation cares about *did propagation occur in this cell during
|
|
this hour*, not about each individual reporter — so the lossy
|
|
aggregation is the desired shape, not just a space optimization.
|
|
|
|
`midpoint_lat/lon` is the great-circle midpoint of the path,
|
|
pre-computed at insert time so HRRR / IEMRE grid lookups are a
|
|
cheap `WHERE midpoint_lat BETWEEN …` rather than a per-row
|
|
recomputation.
|
|
"""
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
|
|
schema "pskr_spots_hourly" do
|
|
field :hour_utc, :utc_datetime
|
|
field :band, :string
|
|
field :sender_grid, :string
|
|
field :receiver_grid, :string
|
|
field :sender_country, :integer
|
|
field :receiver_country, :integer
|
|
field :sender_lat, :float
|
|
field :sender_lon, :float
|
|
field :receiver_lat, :float
|
|
field :receiver_lon, :float
|
|
field :midpoint_lat, :float
|
|
field :midpoint_lon, :float
|
|
field :distance_km, :float
|
|
field :spot_count, :integer, default: 0
|
|
field :max_snr_db, :integer
|
|
field :min_snr_db, :integer
|
|
field :modes, {:array, :string}, default: []
|
|
field :first_spot_at, :utc_datetime
|
|
field :last_spot_at, :utc_datetime
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@type t :: %__MODULE__{}
|
|
|
|
@cast_fields ~w(hour_utc band sender_grid receiver_grid sender_country receiver_country
|
|
sender_lat sender_lon receiver_lat receiver_lon midpoint_lat midpoint_lon
|
|
distance_km spot_count max_snr_db min_snr_db modes first_spot_at
|
|
last_spot_at)a
|
|
|
|
@required_fields ~w(hour_utc band sender_grid receiver_grid spot_count)a
|
|
|
|
@spec changeset(t(), map()) :: Ecto.Changeset.t()
|
|
def changeset(record, attrs) do
|
|
record
|
|
|> cast(attrs, @cast_fields)
|
|
|> validate_required(@required_fields)
|
|
|> unique_constraint([:hour_utc, :band, :sender_grid, :receiver_grid])
|
|
end
|
|
end
|