[feat](trx-server,trx-backend-soapysdr): add sdr gain ceiling

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-28 23:23:41 +01:00
parent dbeec9fb39
commit 93e507606e
3 changed files with 45 additions and 2 deletions
+27
View File
@@ -362,6 +362,9 @@ pub struct SdrGainConfig {
pub mode: String, pub mode: String,
/// Gain in dB; effective only when mode = "manual". /// Gain in dB; effective only when mode = "manual".
pub value: f64, 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<f64>,
} }
impl Default for SdrGainConfig { impl Default for SdrGainConfig {
@@ -369,6 +372,7 @@ impl Default for SdrGainConfig {
Self { Self {
mode: "auto".to_string(), mode: "auto".to_string(),
value: 30.0, 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. // Multi-rig uniqueness checks.
if !self.rigs.is_empty() { if !self.rigs.is_empty() {
let mut seen_ids: std::collections::HashSet<String> = std::collections::HashSet::new(); let mut seen_ids: std::collections::HashSet<String> = 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
));
}
}
} }
} }
+1
View File
@@ -313,6 +313,7 @@ fn build_sdr_rig_from_instance(
&channels, &channels,
&rig_cfg.sdr.gain.mode, &rig_cfg.sdr.gain.mode,
rig_cfg.sdr.gain.value, rig_cfg.sdr.gain.value,
rig_cfg.sdr.gain.max_value,
rig_cfg.audio.sample_rate, rig_cfg.audio.sample_rate,
rig_cfg.audio.channels as usize, rig_cfg.audio.channels as usize,
rig_cfg.audio.frame_duration_ms, rig_cfg.audio.frame_duration_ms,
@@ -55,6 +55,7 @@ impl SoapySdrRig {
/// `(channel_if_hz, initial_mode, audio_bandwidth_hz, fir_taps)`. /// `(channel_if_hz, initial_mode, audio_bandwidth_hz, fir_taps)`.
/// - `gain_mode`: `"auto"` or `"manual"`. /// - `gain_mode`: `"auto"` or `"manual"`.
/// - `gain_db`: gain in dB; used when `gain_mode == "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 /// When `gain_mode == "auto"` hardware AGC is not yet wired, so this
/// value acts as the fallback. /// value acts as the fallback.
/// - `audio_sample_rate`: output PCM rate (Hz). /// - `audio_sample_rate`: output PCM rate (Hz).
@@ -72,6 +73,7 @@ impl SoapySdrRig {
channels: &[(f64, RigMode, u32, usize)], channels: &[(f64, RigMode, u32, usize)],
gain_mode: &str, gain_mode: &str,
gain_db: f64, gain_db: f64,
max_gain_db: Option<f64>,
audio_sample_rate: u32, audio_sample_rate: u32,
audio_channels: usize, audio_channels: usize,
frame_duration_ms: u16, frame_duration_ms: u16,
@@ -83,10 +85,11 @@ impl SoapySdrRig {
center_offset_hz: i64, center_offset_hz: i64,
) -> DynResult<Self> { ) -> DynResult<Self> {
tracing::info!( tracing::info!(
"initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={})", "initialising SoapySDR backend (args={:?}, gain_mode={:?}, gain_db={}, max_gain_db={:?})",
args, args,
gain_mode, gain_mode,
gain_db, gain_db,
max_gain_db,
); );
if gain_mode == "auto" { 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 hardware tunes `center_offset_hz` below the dial frequency so
// the desired signal avoids the DC spike. The DSP mixer compensates. // the desired signal avoids the DC spike. The DSP mixer compensates.
let hardware_center_hz = initial_freq.hz as i64 - center_offset_hz; let hardware_center_hz = initial_freq.hz as i64 - center_offset_hz;
@@ -107,7 +121,7 @@ impl SoapySdrRig {
hardware_center_hz as f64, hardware_center_hz as f64,
sdr_sample_rate as f64, sdr_sample_rate as f64,
bandwidth_hz as f64, bandwidth_hz as f64,
gain_db, effective_gain_db,
)?); )?);
let pipeline = dsp::SdrPipeline::start( let pipeline = dsp::SdrPipeline::start(
@@ -198,6 +212,7 @@ impl SoapySdrRig {
&[], // no channels — pipeline does nothing; filter defaults applied in new_with_config &[], // no channels — pipeline does nothing; filter defaults applied in new_with_config
"auto", "auto",
30.0, 30.0,
None,
48_000, 48_000,
1, 1,
20, 20,