prop/test/microwaveprop_web/live/eme_live_test.exs
Graham McIntire bd107ee747
feat(eme): add /eme Earth-Moon-Earth calculator
New /eme page — antenna aiming + link budget for moonbounce.

Given a station (callsign / grid / lat,lon), TX power, antenna gain,
detection bandwidth, and system Tsys, the page shows in real time:

- Moon azimuth / elevation from the observer (ticks every 10 s)
- Slant range to the Moon
- EIRP breakdown: TX(dBm) + gain(dBi) = EIRP(dBm)
- Path-loss decomposition (Earth → Moon → Earth):
    free-space spreading (×2)
    − moon reflection gain 10·log(4π σ / λ²) where σ = ρ·π·R²
    = round-trip path loss
- Band-dependent lunar albedo ρ (VHF ≈ 0.065, microwave ≈ 0.08,
  mm-wave ≈ 0.02) surfaced as a named line item with the current
  ρ shown to three decimals
- Received power, thermal noise floor, and SNR with a badge that
  classifies the margin (strong / marginal / below noise)
- Self-echo Doppler shift with its radial-velocity driver

New modules:

- `Microwaveprop.Moon` — low-precision lunar ephemeris
  (Astronomical Almanac Sec. D.4 truncated series). Provides
  `julian_day/1`, `geocentric/1`, `observer_position/3`, and
  `radial_velocity_km_s/3`. Accurate to ~0.3° position / ~200 km
  distance — well inside any amateur EME-dish beamwidth.
- `Microwaveprop.Propagation.Eme` — path-loss helpers:
  `moon_albedo/1` (band lookup), `moon_rcs_m2/1`,
  `fspl_round_trip_db/2`, `moon_reflection_gain_db/2`,
  `path_loss_db/3`, `received_power_dbm/1`, `noise_floor_dbm/2`,
  `snr_db/1`, `doppler_shift_hz/2`.

Wired into the main nav ("EME" button) and the router between
/path and /rover.

Tests: 3 properties + 41 unit tests cover JD conversion, moon
position envelope, band-dependent albedo lookup, decomposition
identity, linearity/monotonicity of path loss and Doppler, and a
LiveView integration test against the rendered DOM.

Full suite: 2,460 tests + 170 properties; credo strict clean.
2026-04-23 16:18:20 -05:00

58 lines
1.7 KiB
Elixir

defmodule MicrowavepropWeb.EmeLiveTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
describe "GET /eme" do
test "renders the empty form when no source is supplied", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme")
assert html =~ "EME Calculator"
assert html =~ "Station (callsign, grid, or"
assert html =~ "Calculate"
# No aim readout yet.
refute html =~ "Antenna aim"
end
test "renders the live Moon aim + link budget when a grid source is provided", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme?source=EM13&band=1296&tx_power_dbm=50&tx_gain_dbi=30")
assert html =~ "Antenna aim"
assert html =~ "Link budget"
assert html =~ "EM13"
assert html =~ "Azimuth"
assert html =~ "Elevation"
assert html =~ "Slant range"
# Path loss should be in the high 200s (dB) for 1296 MHz.
assert html =~ "dB"
end
test "submitting the form patches the URL with the supplied params", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/eme")
view
|> form("form",
source: "EM13",
band: "432",
tx_power_dbm: "60",
tx_gain_dbi: "25",
bandwidth_hz: "100",
t_sys_k: "150"
)
|> render_submit()
# Flush any pending assign updates to catch redirects/patches.
html = render(view)
assert html =~ "EM13"
assert html =~ "Antenna aim"
end
test "coordinate pair 'lat,lon' resolves without touching the callsign API", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme?source=32.9,-97.0&band=1296")
assert html =~ "Antenna aim"
assert html =~ "32.9"
assert html =~ "-97.0"
end
end
end