POST /api/v1/beacon-monitor/measurements accepts one measurement per integration window from a propmonitor client, authenticated by the BeaconMonitor token. Records noise floor, signal peak/avg dBFS, SNR, signal-active-fraction, gain, and frequency for later correlation against weather and propagation scores. Beacon UUID must resolve to an approved + on-the-air beacon (404 otherwise — the client drops 404s without retry). Retries with the same (monitor, beacon, measured_at) are idempotent: a unique index makes the second insert a 409. Every accepted upload stamps the monitor's last_seen_at — measurements *are* the heartbeat. MonitorAuth is a separate plug from the user API-token Auth plug; monitors are not users. The rate limiter buckets each monitor by id so retry storms after a 5xx don't burn the shared anon-IP bucket.
76 lines
2.7 KiB
Elixir
76 lines
2.7 KiB
Elixir
defmodule Microwaveprop.BeaconMeasurements.BeaconMeasurement do
|
|
@moduledoc """
|
|
A single beacon-signal-level measurement uploaded by a propmonitor
|
|
client. One row per integration window per (monitor, beacon).
|
|
|
|
All `*_dbfs` fields are uncalibrated and relative to the SDR ADC
|
|
full-scale; `gain_db` is the SDR's reported gain at upload time and
|
|
is needed to interpret dBFS trends across gain changes. SNR is
|
|
already gain-independent so it is the preferred calibration input.
|
|
|
|
`signal_*_dbfs` and `snr_*_db` are computed only over FFT frames
|
|
whose in-band power exceeded `noise_floor + 3 dB`; `signal_active_fraction`
|
|
reports the duty cycle of those frames. When `signal_active_fraction
|
|
== 0.0`, no signal was heard and the peak/avg fields sit near the
|
|
noise floor (SNR ≈ 0 dB).
|
|
"""
|
|
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
alias Microwaveprop.BeaconMonitors.BeaconMonitor
|
|
alias Microwaveprop.Beacons.Beacon
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
schema "beacon_measurements" do
|
|
belongs_to :beacon, Beacon
|
|
belongs_to :monitor, BeaconMonitor
|
|
|
|
field :measured_at, :utc_datetime
|
|
field :frequency_hz, :integer
|
|
field :integration_s, :integer
|
|
field :passband_hz, :float
|
|
field :gain_db, :float
|
|
|
|
field :noise_floor_dbfs, :float
|
|
field :signal_peak_dbfs, :float
|
|
field :signal_avg_dbfs, :float
|
|
field :snr_peak_db, :float
|
|
field :snr_avg_db, :float
|
|
field :signal_active_fraction, :float
|
|
|
|
field :propmonitor_version, :string
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@type t :: %__MODULE__{}
|
|
|
|
@cast_fields ~w(beacon_id monitor_id measured_at frequency_hz integration_s
|
|
passband_hz gain_db noise_floor_dbfs signal_peak_dbfs
|
|
signal_avg_dbfs snr_peak_db snr_avg_db
|
|
signal_active_fraction propmonitor_version)a
|
|
|
|
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
|
def changeset(measurement, attrs) do
|
|
measurement
|
|
|> cast(attrs, @cast_fields)
|
|
|> validate_required(@cast_fields)
|
|
|> validate_number(:frequency_hz, greater_than: 0)
|
|
|> validate_number(:integration_s, greater_than_or_equal_to: 5, less_than_or_equal_to: 3600)
|
|
|> validate_number(:passband_hz, greater_than: 0.0)
|
|
|> validate_number(:gain_db, greater_than_or_equal_to: -10.0, less_than_or_equal_to: 80.0)
|
|
|> validate_number(:signal_active_fraction,
|
|
greater_than_or_equal_to: 0.0,
|
|
less_than_or_equal_to: 1.0
|
|
)
|
|
|> validate_length(:propmonitor_version, max: 32)
|
|
|> foreign_key_constraint(:beacon_id)
|
|
|> foreign_key_constraint(:monitor_id)
|
|
|> unique_constraint([:monitor_id, :beacon_id, :measured_at],
|
|
name: :beacon_measurements_monitor_beacon_measured_at_idx
|
|
)
|
|
end
|
|
end
|