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