feat(beacons): plot-path link seeds TX chain to match beacon EIRP

The "Plot path to beacon" action now includes `tx_power_dbm` and
`src_gain_dbi` in the /path query string so the path calculator's TX
side emits the same EIRP as the beacon out of the box. The beacon
schema stores `power_mw` as EIRP already (per Beacons.RangeEstimate),
so we take 10·log10(power_mw), split against a default 30 dBi dish,
and clamp the TX power to a 0 dBm floor so very-low-EIRP beacons
produce a sane gain/power split instead of -10 dBm outputs.

Examples:
  10 W EIRP (40 dBm)  → 30 dBi dish + 10 dBm TX
  100 W EIRP (50 dBm) → 30 dBi dish + 20 dBm TX
  100 mW EIRP (20 dBm) → 20 dBi + 0 dBm (clamped)
This commit is contained in:
Graham McIntire 2026-04-18 10:54:38 -05:00
parent fbec454917
commit 5dea91d43f
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 50 additions and 1 deletions

View file

@ -285,11 +285,41 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
"dst_height_ft" => to_string(beacon.height_ft)
}
params = maybe_put(base, "band", BandResolver.resolve_as_string(beacon.frequency_mhz))
{tx_power_dbm, src_gain_dbi} = baseline_tx_for_eirp(beacon.power_mw)
params =
base
|> maybe_put("band", BandResolver.resolve_as_string(beacon.frequency_mhz))
|> maybe_put("tx_power_dbm", tx_power_dbm)
|> maybe_put("src_gain_dbi", src_gain_dbi)
"/path?" <> URI.encode_query(params)
end
# Splits the beacon's EIRP (the beacon schema's `power_mw` field is
# already an EIRP value, per Beacons.RangeEstimate) into a
# tx_power_dbm + src_gain_dbi pair so the path calculator's TX chain
# emits the same EIRP as the beacon. We default the antenna gain to
# 30 dBi (typical 2-3 ft microwave dish) and derive the TX power from
# there, clamping to a 0 dBm / 1 mW minimum so very-low-EIRP beacons
# produce sane baseline values instead of -10 dBm absurdities.
defp baseline_tx_for_eirp(nil), do: {nil, nil}
defp baseline_tx_for_eirp(power_mw) when power_mw <= 0, do: {nil, nil}
defp baseline_tx_for_eirp(power_mw) do
eirp_dbm = 10.0 * :math.log10(power_mw)
default_gain = 30.0
min_tx_dbm = 0.0
tx_dbm = max(min_tx_dbm, eirp_dbm - default_gain)
gain_dbi = eirp_dbm - tx_dbm
{format_dbm(tx_dbm), format_dbi(gain_dbi)}
end
defp format_dbm(v), do: v |> Float.round(1) |> to_string()
defp format_dbi(v), do: v |> Float.round(1) |> to_string()
defp precise_grid(%Beacon{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
Maidenhead.from_latlon(lat, lon, 8)
end

View file

@ -92,6 +92,25 @@ defmodule MicrowavepropWeb.BeaconLiveTest do
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
assert html =~ "Pending approval"
end
test "plot-path-to-beacon link pre-populates tx_power + gain to match beacon EIRP",
%{conn: conn} do
# power_mw: 10_000 → EIRP 40 dBm → default-gain 30 dBi → tx 10 dBm
beacon = approved_beacon_fixture(user_fixture(), power_mw: 10_000.0)
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
assert html =~ "tx_power_dbm=10.0"
assert html =~ "src_gain_dbi=30.0"
end
test "low-EIRP beacon clamps tx_power to 0 dBm floor and lowers gain", %{conn: conn} do
# power_mw: 100 → EIRP 20 dBm → tx 0 dBm (clamped), gain 20 dBi
beacon = approved_beacon_fixture(user_fixture(), power_mw: 100.0, callsign: "W5LOW")
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
assert html =~ "tx_power_dbm=0.0"
assert html =~ "src_gain_dbi=20.0"
end
end
describe "Form" do