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