[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,
/// 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<f64>,
}
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<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,
&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,
@@ -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<f64>,
audio_sample_rate: u32,
audio_channels: usize,
frame_duration_ms: u16,
@@ -83,10 +85,11 @@ impl SoapySdrRig {
center_offset_hz: i64,
) -> DynResult<Self> {
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,