defmodule MicrowavepropWeb.MonitorLive.Show do @moduledoc "User-facing beacon-monitor detail at `/beacon-monitors/:id`." use MicrowavepropWeb, :live_view import Ecto.Query alias Microwaveprop.BeaconMeasurements alias Microwaveprop.BeaconMonitors alias Microwaveprop.BeaconMonitors.BeaconMonitor alias Microwaveprop.Beacons.Beacon alias Microwaveprop.Repo @impl true def mount(%{"id" => id}, _session, socket) do user = socket.assigns.current_scope.user monitor = BeaconMonitors.get_monitor!(id) if monitor.user_id == user.id do beacons = Repo.all(from b in Beacon, where: b.approved == true, order_by: b.callsign) measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20) {:ok, socket |> assign(:page_title, "My Monitor: #{monitor.name}") |> assign(:monitor, monitor) |> assign(:beacons, beacons) |> assign(:measurements, measurements) |> assign_form(monitor)} else {:ok, socket |> put_flash(:error, "You don't have access to that monitor.") |> push_navigate(to: ~p"/users/settings")} end end @impl true def render(assigns) do ~H""" <.header> {@monitor.name} <:subtitle>Your beacon monitor

Hardware

Type
{@monitor.hardware_type}
Serial / ID
{@monitor.hardware_id}
Firmware
{@monitor.firmware_version}
Antenna
{@monitor.antenna_type}
Antenna gain
{@monitor.antenna_gain_dbi && "#{@monitor.antenna_gain_dbi} dBi"}
Location
<%= if @monitor.lat && @monitor.lon do %> {@monitor.lat}, {@monitor.lon} <% else %> <% end %>

Connection

Auth token
{@monitor.token}
Last seen
<%= if @monitor.last_seen_at do %> {Calendar.strftime(@monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")} <% else %> Never <% end %>
Status
{if monitor_online?(@monitor), do: "Online", else: "Offline"}

Configuration

Update what your monitor listens for.

<.form for={@form} id="config-form" phx-submit="update-config">
<.input field={@form[:name]} type="text" label="Name" required /> <.input field={@form[:beacon_id]} type="select" label="Beacon to monitor" options={beacon_options(@beacons)} prompt="Select beacon" /> <.input field={@form[:config_frequency_hz]} type="number" label="Frequency (Hz)" />

Override — leave blank to use beacon's frequency

<.input field={@form[:config_integration_s]} type="number" label="Integration (s)" />

Override — leave blank for default (60s)

<.input field={@form[:config_mode]} type="text" label="Mode" placeholder="e.g. wspr, q65a_30" />
<.button variant="primary" phx-disable-with="Saving...">Save

Recent measurements

<%= if @measurements == [] do %>

No measurements yet. Once the monitor starts reporting, they'll appear here.

<% else %>
Time Frequency SNR avg SNR peak Noise floor Gain Integration
{Calendar.strftime(m.measured_at, "%H:%M UTC")} {format_freq(m.frequency_hz)} {Float.round(m.snr_avg_db, 1)} dB {Float.round(m.snr_peak_db, 1)} dB {Float.round(m.noise_floor_dbfs, 1)} dBFS {Float.round(m.gain_db, 1)} dB {m.integration_s}s
<% end %>
""" end @impl true def handle_event("update-config", %{"beacon_monitor" => params}, socket) do monitor = socket.assigns.monitor case BeaconMonitors.update_config(monitor, params) do {:ok, updated} -> {:noreply, socket |> assign(:monitor, updated) |> assign_form(updated) |> put_flash(:info, "Configuration updated.")} {:error, changeset} -> {:noreply, assign(socket, :form, to_form(changeset, as: :beacon_monitor))} end end defp monitor_online?(monitor) do monitor.last_seen_at && DateTime.diff(DateTime.utc_now(), monitor.last_seen_at, :minute) < 30 end defp assign_form(socket, monitor) do assign(socket, :form, to_form(BeaconMonitor.config_changeset(monitor, %{}), as: :beacon_monitor)) end defp beacon_options(beacons) do Enum.map(beacons, &{"#{&1.callsign} @ #{Beacon.format_freq(&1.frequency_mhz)} MHz", &1.id}) end defp format_freq(hz) when is_integer(hz) and hz >= 1_000_000 do mhz = (hz / 1_000_000) |> Float.round(3) |> then(fn n when n == trunc(n) -> trunc(n) n -> n end) "#{mhz} MHz" end defp format_freq(hz) when is_integer(hz), do: "#{hz} Hz" defp format_freq(_), do: "" end