From 93e507606e2ff6af0c726da1070afb1a587868d0 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 28 Feb 2026 23:23:41 +0100 Subject: [PATCH] [feat](trx-server,trx-backend-soapysdr): add sdr gain ceiling Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- src/trx-server/src/config.rs | 27 +++++++++++++++++++ src/trx-server/src/main.rs | 1 + .../trx-backend-soapysdr/src/lib.rs | 19 +++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/trx-server/src/config.rs b/src/trx-server/src/config.rs index b9874e5..d99dc3f 100644 --- a/src/trx-server/src/config.rs +++ b/src/trx-server/src/config.rs @@ -362,6 +362,9 @@ pub struct SdrGainConfig { pub mode: String, /// Gain in dB; effective only when mode = "manual". pub value: f64, + /// Optional hard ceiling for the applied hardware gain in dB. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_value: Option, } impl Default for SdrGainConfig { @@ -369,6 +372,7 @@ impl Default for SdrGainConfig { Self { mode: "auto".to_string(), value: 30.0, + max_value: None, } } } @@ -501,6 +505,15 @@ impl ServerConfig { } } + if let Some(max_gain) = self.sdr.gain.max_value { + if !max_gain.is_finite() { + return Err("[sdr.gain].max_value must be finite".to_string()); + } + if max_gain < 0.0 { + return Err("[sdr.gain].max_value must be >= 0".to_string()); + } + } + // Multi-rig uniqueness checks. if !self.rigs.is_empty() { let mut seen_ids: std::collections::HashSet = std::collections::HashSet::new(); @@ -520,6 +533,20 @@ impl ServerConfig { )); } } + if let Some(max_gain) = rig.sdr.gain.max_value { + if !max_gain.is_finite() { + return Err(format!( + "[[rigs]] [sdr.gain].max_value must be finite (rig id: \"{}\")", + rig.id + )); + } + if max_gain < 0.0 { + return Err(format!( + "[[rigs]] [sdr.gain].max_value must be >= 0 (rig id: \"{}\")", + rig.id + )); + } + } } } diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 5002166..74f5b94 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -313,6 +313,7 @@ fn build_sdr_rig_from_instance( &channels, &rig_cfg.sdr.gain.mode, rig_cfg.sdr.gain.value, + rig_cfg.sdr.gain.max_value, rig_cfg.audio.sample_rate, rig_cfg.audio.channels as usize, rig_cfg.audio.frame_duration_ms, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs index a1e911f..9981df9 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs @@ -55,6 +55,7 @@ impl SoapySdrRig { /// `(channel_if_hz, initial_mode, audio_bandwidth_hz, fir_taps)`. /// - `gain_mode`: `"auto"` or `"manual"`. /// - `gain_db`: gain in dB; used when `gain_mode == "manual"`. + /// - `max_gain_db`: optional hard ceiling for the applied hardware gain. /// When `gain_mode == "auto"` hardware AGC is not yet wired, so this /// value acts as the fallback. /// - `audio_sample_rate`: output PCM rate (Hz). @@ -72,6 +73,7 @@ impl SoapySdrRig { channels: &[(f64, RigMode, u32, usize)], gain_mode: &str, gain_db: f64, + max_gain_db: Option, audio_sample_rate: u32, audio_channels: usize, frame_duration_ms: u16, @@ -83,10 +85,11 @@ impl SoapySdrRig { center_offset_hz: i64, ) -> DynResult { tracing::info!( - "initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={})", + "initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})", args, gain_mode, gain_db, + max_gain_db, ); if gain_mode == "auto" { @@ -97,6 +100,17 @@ impl SoapySdrRig { ); } + let effective_gain_db = max_gain_db + .map(|max_gain| gain_db.min(max_gain)) + .unwrap_or(gain_db); + if (effective_gain_db - gain_db).abs() > f64::EPSILON { + tracing::info!( + "Clamping SoapySDR gain from {} dB to {} dB due to configured max_value", + gain_db, + effective_gain_db, + ); + } + // The hardware tunes `center_offset_hz` below the dial frequency so // the desired signal avoids the DC spike. The DSP mixer compensates. let hardware_center_hz = initial_freq.hz as i64 - center_offset_hz; @@ -107,7 +121,7 @@ impl SoapySdrRig { hardware_center_hz as f64, sdr_sample_rate as f64, bandwidth_hz as f64, - gain_db, + effective_gain_db, )?); let pipeline = dsp::SdrPipeline::start( @@ -198,6 +212,7 @@ impl SoapySdrRig { &[], // no channels — pipeline does nothing; filter defaults applied in new_with_config "auto", 30.0, + None, 48_000, 1, 20,