For a 1 W / 432 MHz beacon, RangeEstimate.estimate/1 was returning ~126 k cells because free-space loss dominates over atmospheric loss at low UHF, letting the rx_dbm filter pass almost the whole bbox. Jason-encoding that payload into the map's data-cells attribute was locking up browsers on the beacon detail page for low-band beacons even though the coverage toggle is already disabled for them. Return an empty estimate below the same 5760 MHz threshold the UI already enforces.
134 lines
4.3 KiB
Elixir
134 lines
4.3 KiB
Elixir
defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
|
use Microwaveprop.DataCase
|
|
|
|
alias Microwaveprop.Beacons.Beacon
|
|
alias Microwaveprop.Beacons.RangeEstimate
|
|
|
|
defp beacon(overrides \\ %{}) do
|
|
base = %Beacon{
|
|
id: "00000000-0000-0000-0000-000000000000",
|
|
frequency_mhz: 10_368.1,
|
|
callsign: "W5HN",
|
|
grid: "EM12",
|
|
lat: 32.897,
|
|
lon: -97.038,
|
|
power_mw: 10_000.0,
|
|
height_ft: 100,
|
|
on_the_air: true
|
|
}
|
|
|
|
struct!(base, overrides)
|
|
end
|
|
|
|
describe "mw_to_dbm/1" do
|
|
test "1 mW is 0 dBm" do
|
|
assert_in_delta RangeEstimate.mw_to_dbm(1.0), 0.0, 0.001
|
|
end
|
|
|
|
test "1 W (1000 mW) is 30 dBm" do
|
|
assert_in_delta RangeEstimate.mw_to_dbm(1000.0), 30.0, 0.001
|
|
end
|
|
|
|
test "10 W (10_000 mW) is 40 dBm" do
|
|
assert_in_delta RangeEstimate.mw_to_dbm(10_000.0), 40.0, 0.001
|
|
end
|
|
end
|
|
|
|
describe "nearest_band_mhz/1" do
|
|
test "10368.1 MHz maps to 10000 (10 GHz band)" do
|
|
assert RangeEstimate.nearest_band_mhz(10_368.1) == 10_000
|
|
end
|
|
|
|
test "24192.1 MHz maps to 24000" do
|
|
assert RangeEstimate.nearest_band_mhz(24_192.1) == 24_000
|
|
end
|
|
|
|
test "47088 MHz maps to 47000" do
|
|
assert RangeEstimate.nearest_band_mhz(47_088.0) == 47_000
|
|
end
|
|
end
|
|
|
|
describe "estimate/1" do
|
|
test "returns a map with band info, center score, and per-grid cells" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
assert result.band_mhz == 10_000
|
|
assert is_integer(result.center_score) or is_float(result.center_score)
|
|
assert is_float(result.eirp_dbm)
|
|
assert is_list(result.cells)
|
|
assert result.cells != []
|
|
assert result.grid_step == 0.125
|
|
assert is_list(result.tiers)
|
|
end
|
|
|
|
test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
for cell <- result.cells do
|
|
assert is_float(cell.lat)
|
|
assert is_float(cell.lon)
|
|
assert is_float(cell.distance_km)
|
|
assert is_integer(cell.rx_dbm)
|
|
assert is_binary(cell.label)
|
|
assert is_binary(cell.color)
|
|
# Detection floor enforced
|
|
assert cell.rx_dbm >= -145
|
|
end
|
|
end
|
|
|
|
test "cells closer to the beacon have stronger rx_dbm" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
[closest | _] = Enum.sort_by(result.cells, & &1.distance_km)
|
|
[farthest | _] = Enum.sort_by(result.cells, &(-&1.distance_km))
|
|
|
|
assert closest.rx_dbm > farthest.rx_dbm
|
|
end
|
|
|
|
test "a more powerful beacon produces more cells" do
|
|
weak = RangeEstimate.estimate(beacon(power_mw: 10.0))
|
|
strong = RangeEstimate.estimate(beacon(power_mw: 10_000.0))
|
|
assert length(strong.cells) > length(weak.cells)
|
|
end
|
|
|
|
test "uses a default score of 50 when no propagation data exists" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
# No scores in test DB → center defaults, cells all use 50.
|
|
assert result.center_score == 50
|
|
assert result.valid_time == nil
|
|
|
|
# All cells should have score 50 when there's no HRRR data.
|
|
assert Enum.all?(result.cells, fn c -> c.score == 50 end)
|
|
end
|
|
|
|
test "cells align with the 0.125° HRRR grid" do
|
|
result = RangeEstimate.estimate(beacon())
|
|
|
|
for cell <- result.cells do
|
|
# lat and lon should both be multiples of 0.125
|
|
lat_mod = abs(cell.lat / 0.125 - round(cell.lat / 0.125))
|
|
lon_mod = abs(cell.lon / 0.125 - round(cell.lon / 0.125))
|
|
assert lat_mod < 0.01
|
|
assert lon_mod < 0.01
|
|
end
|
|
end
|
|
|
|
test "short-circuits with empty cells for beacons below 5.76 GHz" do
|
|
# 432 MHz with 1 W at unrestricted range would generate >100k cells
|
|
# because atmospheric loss is negligible — encoding that payload
|
|
# into the map's data attribute was locking up browsers in prod.
|
|
result = RangeEstimate.estimate(beacon(frequency_mhz: 432.38, power_mw: 1000.0))
|
|
assert result.cells == []
|
|
assert result.max_range_km == 0.0
|
|
# Still reports the nearest configured band for display purposes.
|
|
assert is_integer(result.band_mhz)
|
|
end
|
|
|
|
test "short-circuits at exactly 5760 MHz threshold" do
|
|
above = RangeEstimate.estimate(beacon(frequency_mhz: 5760.0, power_mw: 1000.0))
|
|
below = RangeEstimate.estimate(beacon(frequency_mhz: 5759.9, power_mw: 1000.0))
|
|
assert above.cells != []
|
|
assert below.cells == []
|
|
end
|
|
end
|
|
end
|