defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do @moduledoc """ A beacon monitor is a physical SDR-based hardware unit assigned to a user. The unit runs the `propmonitor` client which reports beacon reception measurements back to Microwaveprop. Each monitor has a unique random token the client uses to authenticate its measurement uploads. """ use Ecto.Schema import Ecto.Changeset alias Microwaveprop.Accounts.User alias Microwaveprop.Beacons.Beacon @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "beacon_monitors" do # Identity / label field :name, :string field :token, :string field :last_seen_at, :utc_datetime # Hardware identity field :hardware_type, :string field :hardware_id, :string field :firmware_version, :string # Antenna / installation field :antenna_type, :string field :antenna_gain_dbi, :float field :lat, :float field :lon, :float # Active configuration (admin + assigned user can change) field :config_frequency_hz, :integer field :config_integration_s, :integer field :config_mode, :string # Relationships belongs_to :user, User belongs_to :beacon, Beacon belongs_to :assigned_by, User timestamps(type: :utc_datetime) end @type t :: %__MODULE__{} @doc """ General changeset — allows basic fields. Context-level functions enforce which caller-role can set which fields. """ @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def changeset(monitor, attrs) do monitor |> cast(attrs, [:name]) |> validate_required([:name]) |> validate_length(:name, min: 1, max: 100) end @doc """ Provisioning changeset — used by admins to register new hardware. Casts all hardware-identity, antenna, and assignment fields. """ @spec provision_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def provision_changeset(monitor, attrs) do monitor |> cast(attrs, [ :name, :hardware_type, :hardware_id, :firmware_version, :antenna_type, :antenna_gain_dbi, :lat, :lon, :user_id, :assigned_by_id ]) |> validate_required([:name]) |> validate_length(:name, min: 1, max: 100) |> validate_length(:hardware_type, max: 100) |> validate_length(:hardware_id, max: 255) |> validate_length(:firmware_version, max: 50) |> validate_length(:antenna_type, max: 100) |> validate_number(:antenna_gain_dbi, greater_than_or_equal_to: -20, less_than_or_equal_to: 60) |> validate_number(:lat, greater_than_or_equal_to: -90, less_than_or_equal_to: 90) |> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180) |> foreign_key_constraint(:user_id) |> foreign_key_constraint(:assigned_by_id) end @doc """ Configuration changeset — used by admins and assigned users to update what the monitor is listening for. """ @spec config_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def config_changeset(monitor, attrs) do monitor |> cast(attrs, [:name, :beacon_id, :config_frequency_hz, :config_integration_s, :config_mode]) |> validate_required([:name]) |> validate_length(:name, min: 1, max: 100) |> validate_length(:config_mode, max: 50) |> validate_number(:config_frequency_hz, greater_than: 0) |> validate_number(:config_integration_s, greater_than_or_equal_to: 5, less_than_or_equal_to: 3600) |> foreign_key_constraint(:beacon_id) end end