[feat](trx-client): configure spectrum guard tuning

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-02 20:00:50 +01:00
parent 5ed55c6103
commit 50f8c12487
5 changed files with 75 additions and 6 deletions
+25
View File
@@ -238,6 +238,10 @@ pub struct HttpFrontendConfig {
pub default_rig_id: Option<String>,
/// Initial zoom level for the APRS map when receiver coordinates are known.
pub initial_map_zoom: u8,
/// Spectrum center-retune guard margin on each side of the selected bandwidth.
pub spectrum_coverage_margin_hz: u32,
/// Fraction of the sampled spectrum span treated as usable for center-retune logic.
pub spectrum_usable_span_ratio: f32,
/// Whether to expose the RF Gain control in the web UI.
pub show_sdr_gain_control: bool,
/// Authentication settings
@@ -252,6 +256,8 @@ impl Default for HttpFrontendConfig {
port: 8080,
default_rig_id: None,
initial_map_zoom: 10,
spectrum_coverage_margin_hz: 50_000,
spectrum_usable_span_ratio: 1.0,
show_sdr_gain_control: true,
auth: HttpAuthConfig::default(),
}
@@ -364,6 +370,17 @@ impl ClientConfig {
if self.frontends.http.initial_map_zoom == 0 {
return Err("[frontends.http].initial_map_zoom must be > 0".to_string());
}
if self.frontends.http.spectrum_coverage_margin_hz == 0 {
return Err("[frontends.http].spectrum_coverage_margin_hz must be > 0".to_string());
}
if !(self.frontends.http.spectrum_usable_span_ratio > 0.0
&& self.frontends.http.spectrum_usable_span_ratio <= 1.0)
{
return Err(
"[frontends.http].spectrum_usable_span_ratio must be > 0.0 and <= 1.0"
.to_string(),
);
}
if self.frontends.rigctl.enabled && self.frontends.rigctl.rig_ports.is_empty() {
return Err(
"[frontends.rigctl].rig_ports must contain at least one rig when enabled"
@@ -459,6 +476,8 @@ impl ClientConfig {
port: 8080,
default_rig_id: Some("hf".to_string()),
initial_map_zoom: 10,
spectrum_coverage_margin_hz: 50_000,
spectrum_usable_span_ratio: 1.0,
show_sdr_gain_control: true,
auth: HttpAuthConfig {
enabled: false,
@@ -559,6 +578,8 @@ mod tests {
assert!(!config.frontends.rigctl.enabled);
assert_eq!(config.frontends.http.port, 8080);
assert_eq!(config.frontends.http.initial_map_zoom, 10);
assert_eq!(config.frontends.http.spectrum_coverage_margin_hz, 50_000);
assert_eq!(config.frontends.http.spectrum_usable_span_ratio, 1.0);
assert_eq!(config.frontends.rigctl.port, 4532);
assert!(config.frontends.http_json.enabled);
assert_eq!(config.frontends.http_json.port, 0);
@@ -593,6 +614,8 @@ enabled = true
listen = "127.0.0.1"
port = 8080
initial_map_zoom = 12
spectrum_coverage_margin_hz = 40000
spectrum_usable_span_ratio = 0.9
"#;
@@ -606,6 +629,8 @@ initial_map_zoom = 12
assert_eq!(config.remote.poll_interval_ms, 500);
assert!(config.frontends.http.enabled);
assert_eq!(config.frontends.http.initial_map_zoom, 12);
assert_eq!(config.frontends.http.spectrum_coverage_margin_hz, 40_000);
assert_eq!(config.frontends.http.spectrum_usable_span_ratio, 0.9);
}
#[test]
+4
View File
@@ -179,6 +179,10 @@ async fn async_init() -> DynResult<AppState> {
};
frontend_runtime.http_show_sdr_gain_control = cfg.frontends.http.show_sdr_gain_control;
frontend_runtime.http_initial_map_zoom = cfg.frontends.http.initial_map_zoom;
frontend_runtime.http_spectrum_coverage_margin_hz =
cfg.frontends.http.spectrum_coverage_margin_hz;
frontend_runtime.http_spectrum_usable_span_ratio =
cfg.frontends.http.spectrum_usable_span_ratio;
// Resolve remote URL: CLI > config [remote] section > error
let remote_url = cli
+6
View File
@@ -170,6 +170,10 @@ pub struct FrontendRuntimeContext {
pub http_show_sdr_gain_control: bool,
/// Initial APRS map zoom level when receiver coordinates are available.
pub http_initial_map_zoom: u8,
/// Spectrum center-retune guard margin on each side of the tuned passband.
pub http_spectrum_coverage_margin_hz: u32,
/// Fraction of the sampled spectrum span treated as usable by the web UI.
pub http_spectrum_usable_span_ratio: f32,
/// Currently selected remote rig id (used by remote client routing).
pub remote_active_rig_id: Arc<Mutex<Option<String>>>,
/// Cached remote rig list from GetRigs polling.
@@ -209,6 +213,8 @@ impl FrontendRuntimeContext {
http_auth_cookie_same_site: "Lax".to_string(),
http_show_sdr_gain_control: true,
http_initial_map_zoom: 10,
http_spectrum_coverage_margin_hz: 50_000,
http_spectrum_usable_span_ratio: 1.0,
remote_active_rig_id: Arc::new(Mutex::new(None)),
remote_rigs: Arc::new(Mutex::new(Vec::new())),
owner_callsign: None,
@@ -1167,7 +1167,8 @@ function effectiveSpectrumCoverageSpanHz(sampleRateHz) {
const sampleRate = Number(sampleRateHz);
if (!Number.isFinite(sampleRate) || sampleRate <= 0) return 0;
// Keep a guard band at the spectrum edges; practical usable span is slightly smaller.
return sampleRate * 1.0;
const ratio = Number.isFinite(spectrumUsableSpanRatio) ? spectrumUsableSpanRatio : 1.0;
return sampleRate * Math.max(0.01, Math.min(1.0, ratio));
}
function requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz = coverageGuardBandwidthHz()) {
@@ -1180,7 +1181,7 @@ function requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz = covera
const safeBw = Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0);
const halfSpanHz = sampleRate / 2;
const requiredHalfSpanHz = safeBw / 2 + SPECTRUM_COVERAGE_MARGIN_HZ;
const requiredHalfSpanHz = safeBw / 2 + spectrumCoverageMarginHz;
if (requiredHalfSpanHz * 2 >= sampleRate) {
return alignFreqToRigStep(Math.round(freqHz));
}
@@ -1258,7 +1259,7 @@ function sweetSpotCandidateForFrame(data, freqHz, bandwidthHz) {
const halfUsableSpanHz = usableSpanHz / 2;
const fullHalfSpanHz = sampleRate / 2;
const guardHalfSpanHz = bandwidthHz / 2 + SPECTRUM_COVERAGE_MARGIN_HZ;
const guardHalfSpanHz = bandwidthHz / 2 + spectrumCoverageMarginHz;
if (guardHalfSpanHz * 2 >= usableSpanHz) {
const fallbackCenterHz = requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz);
if (!Number.isFinite(fallbackCenterHz)) return null;
@@ -1346,7 +1347,7 @@ function sweetSpotProbeCenters(data, freqHz, bandwidthHz) {
if (!Number.isFinite(usableSpanHz) || usableSpanHz <= 0) return [];
const halfUsableSpanHz = usableSpanHz / 2;
const guardHalfSpanHz = bandwidthHz / 2 + SPECTRUM_COVERAGE_MARGIN_HZ;
const guardHalfSpanHz = bandwidthHz / 2 + spectrumCoverageMarginHz;
if (guardHalfSpanHz * 2 >= usableSpanHz) {
return [alignFreqToRigStep(Math.round(freqHz))];
}
@@ -1446,7 +1447,7 @@ function tunedFrequencyForCenterCoverage(centerHz, freqHz = lastFreqHz, bandwidt
const safeBw = Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0);
const halfSpanHz = sampleRate / 2;
const requiredHalfSpanHz = safeBw / 2 + SPECTRUM_COVERAGE_MARGIN_HZ;
const requiredHalfSpanHz = safeBw / 2 + spectrumCoverageMarginHz;
if (requiredHalfSpanHz * 2 >= sampleRate) {
return alignFreqToRigStep(Math.round(centerHz));
}
@@ -1650,7 +1651,8 @@ let serverActiveRigId = null;
let serverLat = null;
let serverLon = null;
let initialMapZoom = 10;
const SPECTRUM_COVERAGE_MARGIN_HZ = 50_000;
let spectrumCoverageMarginHz = 50_000;
let spectrumUsableSpanRatio = 1.0;
function updateFooterBuildInfo() {
const serverEl = document.getElementById("footer-server-build");
@@ -1702,6 +1704,18 @@ function render(update) {
if (typeof update.initial_map_zoom === "number" && Number.isFinite(update.initial_map_zoom)) {
initialMapZoom = Math.max(1, Math.round(update.initial_map_zoom));
}
if (
typeof update.spectrum_coverage_margin_hz === "number" &&
Number.isFinite(update.spectrum_coverage_margin_hz)
) {
spectrumCoverageMarginHz = Math.max(1, Math.round(update.spectrum_coverage_margin_hz));
}
if (
typeof update.spectrum_usable_span_ratio === "number" &&
Number.isFinite(update.spectrum_usable_span_ratio)
) {
spectrumUsableSpanRatio = Math.max(0.01, Math.min(1.0, Number(update.spectrum_usable_span_ratio)));
}
updateTitle();
updateFooterBuildInfo();
@@ -41,6 +41,8 @@ struct FrontendMeta {
owner_website_name: Option<String>,
show_sdr_gain_control: bool,
initial_map_zoom: u8,
spectrum_coverage_margin_hz: u32,
spectrum_usable_span_ratio: f32,
}
#[get("/status")]
@@ -99,6 +101,14 @@ fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String {
"initial_map_zoom".to_string(),
serde_json::json!(meta.initial_map_zoom),
);
map.insert(
"spectrum_coverage_margin_hz".to_string(),
serde_json::json!(meta.spectrum_coverage_margin_hz),
);
map.insert(
"spectrum_usable_span_ratio".to_string(),
serde_json::json!(meta.spectrum_usable_span_ratio),
);
serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
}
@@ -118,6 +128,8 @@ fn frontend_meta_from_context(
owner_website_name: owner_website_name_from_context(context),
show_sdr_gain_control: show_sdr_gain_control_from_context(context),
initial_map_zoom: initial_map_zoom_from_context(context),
spectrum_coverage_margin_hz: spectrum_coverage_margin_hz_from_context(context),
spectrum_usable_span_ratio: spectrum_usable_span_ratio_from_context(context),
}
}
@@ -167,6 +179,14 @@ fn initial_map_zoom_from_context(context: &FrontendRuntimeContext) -> u8 {
context.http_initial_map_zoom
}
fn spectrum_coverage_margin_hz_from_context(context: &FrontendRuntimeContext) -> u32 {
context.http_spectrum_coverage_margin_hz
}
fn spectrum_usable_span_ratio_from_context(context: &FrontendRuntimeContext) -> f32 {
context.http_spectrum_usable_span_ratio
}
#[get("/events")]
pub async fn events(
state: web::Data<watch::Receiver<RigState>>,