From 50f8c1248733c023c090676947c4b1c85c2dfde4 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Mon, 2 Mar 2026 20:00:50 +0100 Subject: [PATCH] [feat](trx-client): configure spectrum guard tuning Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- src/trx-client/src/config.rs | 25 ++++++++++++++++++ src/trx-client/src/main.rs | 4 +++ src/trx-client/trx-frontend/src/lib.rs | 6 +++++ .../trx-frontend-http/assets/web/app.js | 26 ++++++++++++++----- .../trx-frontend/trx-frontend-http/src/api.rs | 20 ++++++++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index fcaaa5c..43f603a 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -238,6 +238,10 @@ pub struct HttpFrontendConfig { pub default_rig_id: Option, /// 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] diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index bbaff86..b734ca9 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -179,6 +179,10 @@ async fn async_init() -> DynResult { }; 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 diff --git a/src/trx-client/trx-frontend/src/lib.rs b/src/trx-client/trx-frontend/src/lib.rs index acb227f..764ba31 100644 --- a/src/trx-client/trx-frontend/src/lib.rs +++ b/src/trx-client/trx-frontend/src/lib.rs @@ -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>>, /// 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, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 28eb45d..4a2a19b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -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(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 2347e78..82a4a84 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -41,6 +41,8 @@ struct FrontendMeta { owner_website_name: Option, 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>,