[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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
Reference in New Issue
Block a user