[feat](trx-backend-soapysdr): implement LNA gain element control

Add set_named_gain to the IqSource trait and implement it in
RealIqSource via soapysdr Device::set_gain_element. Wire a
lna_gain_cmd channel through SdrPipeline so the IQ read loop applies
LNA gain changes on the next iteration. Add set_sdr_lna_gain to
SoapySdrRig and expose the current value via filter_state.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-15 19:25:04 +01:00
parent fb715860fb
commit 2a298add7d
3 changed files with 53 additions and 0 deletions
@@ -57,6 +57,12 @@ pub trait IqSource: Send + 'static {
Ok(())
}
/// Apply a new gain to a named gain element (e.g. "LNA"). Default
/// implementation is a no-op for sources that do not support named gains.
fn set_named_gain(&mut self, _name: &str, _gain_db: f64) -> Result<(), String> {
Ok(())
}
/// Enable or disable hardware automatic gain control. Default
/// implementation is a no-op for sources that do not support AGC.
fn set_gain_mode(&mut self, _automatic: bool) -> Result<(), String> {
@@ -106,6 +112,9 @@ pub struct SdrPipeline {
/// Write `Some(gain_db)` here to adjust the hardware RX gain.
/// The IQ read loop picks it up on the next iteration.
pub gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
/// Write `Some(gain_db)` here to adjust the LNA gain element.
/// The IQ read loop picks it up on the next iteration.
pub lna_gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
/// Write `Some(enabled)` here to switch hardware AGC on or off.
/// The IQ read loop picks it up on the next iteration.
pub agc_cmd: Arc<std::sync::Mutex<Option<bool>>>,
@@ -183,6 +192,9 @@ impl SdrPipeline {
let thread_retune_cmd = retune_cmd.clone();
let gain_cmd: Arc<std::sync::Mutex<Option<f64>>> = Arc::new(std::sync::Mutex::new(None));
let thread_gain_cmd = gain_cmd.clone();
let lna_gain_cmd: Arc<std::sync::Mutex<Option<f64>>> =
Arc::new(std::sync::Mutex::new(None));
let thread_lna_gain_cmd = lna_gain_cmd.clone();
let agc_cmd: Arc<std::sync::Mutex<Option<bool>>> = Arc::new(std::sync::Mutex::new(None));
let thread_agc_cmd = agc_cmd.clone();
@@ -197,6 +209,7 @@ impl SdrPipeline {
thread_spectrum_buf,
thread_retune_cmd,
thread_gain_cmd,
thread_lna_gain_cmd,
thread_agc_cmd,
);
})
@@ -210,6 +223,7 @@ impl SdrPipeline {
sdr_sample_rate,
retune_cmd,
gain_cmd,
lna_gain_cmd,
agc_cmd,
shared_center_hz: Arc::new(AtomicI64::new(0)),
audio_sample_rate,
@@ -290,6 +304,7 @@ fn iq_read_loop(
spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
lna_gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
agc_cmd: Arc<std::sync::Mutex<Option<bool>>>,
) {
let mut block = vec![Complex::new(0.0_f32, 0.0_f32); IQ_BLOCK_SIZE];
@@ -323,6 +338,15 @@ fn iq_read_loop(
}
}
}
if let Ok(mut cmd) = lna_gain_cmd.try_lock() {
if let Some(gain_db) = cmd.take() {
if let Err(e) = source.set_named_gain("LNA", gain_db) {
tracing::warn!("SDR LNA gain change to {:.1} dB failed: {}", gain_db, e);
} else {
tracing::info!("SDR LNA gain updated to {:.1} dB", gain_db);
}
}
}
if let Ok(mut cmd) = agc_cmd.try_lock() {
if let Some(enabled) = cmd.take() {
if let Err(e) = source.set_gain_mode(enabled) {
@@ -52,6 +52,8 @@ pub struct SoapySdrRig {
gain_db: f64,
/// Optional hard ceiling for the applied hardware gain in dB.
max_gain_db: Option<f64>,
/// Requested LNA gain element setting in dB (None if not set by user).
lna_gain_db: Option<f64>,
/// Whether hardware AGC is currently enabled.
agc_enabled: bool,
/// Whether software squelch is enabled on primary channel (except WFM mode).
@@ -285,6 +287,7 @@ impl SoapySdrRig {
wfm_denoise: WfmDenoiseLevel::Auto,
gain_db,
max_gain_db,
lna_gain_db: None,
agc_enabled,
squelch_enabled,
squelch_threshold_db,
@@ -579,6 +582,25 @@ impl RigCat for SoapySdrRig {
})
}
fn set_sdr_lna_gain<'a>(
&'a mut self,
gain_db: f64,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
if !gain_db.is_finite() {
return Err("LNA gain must be finite".into());
}
if gain_db < 0.0 {
return Err("LNA gain must be >= 0".into());
}
self.lna_gain_db = Some(gain_db);
if let Ok(mut cmd) = self.pipeline.lna_gain_cmd.lock() {
*cmd = Some(gain_db);
}
Ok(())
})
}
fn set_sdr_agc<'a>(
&'a mut self,
enabled: bool,
@@ -803,6 +825,7 @@ impl RigCat for SoapySdrRig {
.map(|max_gain| self.gain_db.min(max_gain))
.unwrap_or(self.gain_db),
),
sdr_lna_gain_db: self.lna_gain_db,
sdr_agc_enabled: Some(self.agc_enabled),
sdr_squelch_enabled: Some(self.squelch_enabled),
sdr_squelch_threshold_db: Some(self.squelch_threshold_db as f64),
@@ -179,6 +179,12 @@ impl IqSource for RealIqSource {
.map_err(|e| format!("Failed to set SDR gain: {}", e))
}
fn set_named_gain(&mut self, name: &str, gain_db: f64) -> Result<(), String> {
self.device
.set_gain_element(soapysdr::Direction::Rx, 0, name, gain_db)
.map_err(|e| format!("Failed to set SDR {} gain: {}", name, e))
}
fn set_gain_mode(&mut self, automatic: bool) -> Result<(), String> {
self.device
.set_gain_mode(soapysdr::Direction::Rx, 0, automatic)