diff --git a/lib/microwaveprop/propagation/mode_thresholds.ex b/lib/microwaveprop/propagation/mode_thresholds.ex new file mode 100644 index 00000000..bfb1282d --- /dev/null +++ b/lib/microwaveprop/propagation/mode_thresholds.ex @@ -0,0 +1,35 @@ +defmodule Microwaveprop.Propagation.ModeThresholds do + @moduledoc """ + Required-SNR thresholds (dB) for the modes the rover planner supports. + + The rover predicts a per-link SNR margin; subtracting the mode's required + SNR yields "dB above decode threshold". Numbers come from the rover spec — + SSB needs full quieting, CW pulls signals well below the noise floor, and + Q65 modes go deeper still. + """ + + @thresholds %{ + ssb: 0.0, + cw: -18.0, + q65_60a: -24.0, + q65_120b: -26.0 + } + + @modes [ + {:ssb, "SSB"}, + {:cw, "CW"}, + {:q65_60a, "Q65-60A"}, + {:q65_120b, "Q65-120B"} + ] + + @type mode :: :ssb | :cw | :q65_60a | :q65_120b + + @spec required_snr_db(mode()) :: float() + def required_snr_db(mode) when is_map_key(@thresholds, mode), do: Map.fetch!(@thresholds, mode) + + @spec modes() :: [{mode(), String.t()}] + def modes, do: @modes + + @spec default_mode() :: mode() + def default_mode, do: :ssb +end diff --git a/test/microwaveprop/propagation/mode_thresholds_test.exs b/test/microwaveprop/propagation/mode_thresholds_test.exs new file mode 100644 index 00000000..a6ebbdc1 --- /dev/null +++ b/test/microwaveprop/propagation/mode_thresholds_test.exs @@ -0,0 +1,35 @@ +defmodule Microwaveprop.Propagation.ModeThresholdsTest do + use ExUnit.Case, async: true + + alias Microwaveprop.Propagation.ModeThresholds + + describe "required_snr_db/1" do + test "returns the documented thresholds for each supported mode" do + assert ModeThresholds.required_snr_db(:ssb) == 0.0 + assert ModeThresholds.required_snr_db(:cw) == -18.0 + assert ModeThresholds.required_snr_db(:q65_60a) == -24.0 + assert ModeThresholds.required_snr_db(:q65_120b) == -26.0 + end + + test "raises FunctionClauseError for unknown modes" do + assert_raise FunctionClauseError, fn -> ModeThresholds.required_snr_db(:fm) end + end + end + + describe "modes/0" do + test "returns ordered {atom, label} pairs covering every threshold" do + modes = ModeThresholds.modes() + assert {:ssb, "SSB"} in modes + assert {:cw, "CW"} in modes + assert {:q65_60a, "Q65-60A"} in modes + assert {:q65_120b, "Q65-120B"} in modes + assert length(modes) == 4 + end + end + + describe "default_mode/0" do + test "returns the SSB atom" do + assert ModeThresholds.default_mode() == :ssb + end + end +end