[feat](trx-rs): expose live sdr gain control

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:30:08 +01:00
parent 93e507606e
commit 617255cd32
15 changed files with 138 additions and 0 deletions
+8
View File
@@ -452,6 +452,14 @@ async fn process_command(
let _ = ctx.state_tx.send(ctx.state.clone());
return snapshot_from(ctx.state);
}
RigCommand::SetSdrGain(gain_db) => {
if let Err(e) = ctx.rig.set_sdr_gain(gain_db).await {
return Err(RigError::communication(format!("set_sdr_gain: {e}")));
}
ctx.state.filter = ctx.rig.filter_state();
let _ = ctx.state_tx.send(ctx.state.clone());
return snapshot_from(ctx.state);
}
RigCommand::SetWfmDeemphasis(deemphasis_us) => {
if let Err(e) = ctx.rig.set_wfm_deemphasis(deemphasis_us).await {
return Err(RigError::communication(format!("set_wfm_deemphasis: {e}")));
@@ -45,6 +45,12 @@ pub trait IqSource: Send + 'static {
Ok(())
}
/// Apply a new hardware receive gain in dB. Default implementation is a
/// no-op for non-hardware sources.
fn set_gain(&mut self, _gain_db: f64) -> Result<(), String> {
Ok(())
}
/// Gives a source-specific implementation a chance to recover from a
/// read error (for example, by rearming a hardware stream after overflow).
/// Returns `true` when an active recovery action was attempted.
@@ -740,6 +746,9 @@ pub struct SdrPipeline {
/// Write `Some(hz)` here to retune the hardware center frequency.
/// The IQ read loop picks it up on the next iteration.
pub retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
/// 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>>>,
}
impl SdrPipeline {
@@ -785,6 +794,8 @@ impl SdrPipeline {
let thread_spectrum_buf = spectrum_buf.clone();
let retune_cmd: Arc<std::sync::Mutex<Option<f64>>> = Arc::new(std::sync::Mutex::new(None));
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();
std::thread::Builder::new()
.name("sdr-iq-read".to_string())
@@ -796,6 +807,7 @@ impl SdrPipeline {
iq_tx,
thread_spectrum_buf,
thread_retune_cmd,
thread_gain_cmd,
);
})
.expect("failed to spawn sdr-iq-read thread");
@@ -806,6 +818,7 @@ impl SdrPipeline {
spectrum_buf,
sdr_sample_rate,
retune_cmd,
gain_cmd,
}
}
}
@@ -829,6 +842,7 @@ fn iq_read_loop(
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
retune_cmd: Arc<std::sync::Mutex<Option<f64>>>,
gain_cmd: Arc<std::sync::Mutex<Option<f64>>>,
) {
let mut block = vec![Complex::new(0.0_f32, 0.0_f32); IQ_BLOCK_SIZE];
let block_duration_ms = if sdr_sample_rate > 0 {
@@ -859,6 +873,15 @@ fn iq_read_loop(
}
}
}
if let Ok(mut cmd) = gain_cmd.try_lock() {
if let Some(gain_db) = cmd.take() {
if let Err(e) = source.set_gain(gain_db) {
tracing::warn!("SDR gain change to {:.1} dB failed: {}", gain_db, e);
} else {
tracing::info!("SDR gain updated to {:.1} dB", gain_db);
}
}
}
let n = match source.read_into(&mut block) {
Ok(n) => {
@@ -41,6 +41,10 @@ pub struct SoapySdrRig {
wfm_deemphasis_us: u32,
/// Whether WFM stereo decode is enabled.
wfm_stereo: bool,
/// Requested hardware gain setting in dB.
gain_db: f64,
/// Optional hard ceiling for the applied hardware gain in dB.
max_gain_db: Option<f64>,
}
impl SoapySdrRig {
@@ -200,6 +204,8 @@ impl SoapySdrRig {
retune_cmd,
wfm_deemphasis_us,
wfm_stereo: true,
gain_db,
max_gain_db,
})
}
@@ -353,6 +359,29 @@ impl RigCat for SoapySdrRig {
})
}
fn set_sdr_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("gain must be finite".into());
}
if gain_db < 0.0 {
return Err("gain must be >= 0".into());
}
self.gain_db = gain_db;
let effective_gain_db = self
.max_gain_db
.map(|max_gain| gain_db.min(max_gain))
.unwrap_or(gain_db);
if let Ok(mut cmd) = self.pipeline.gain_cmd.lock() {
*cmd = Some(effective_gain_db);
}
Ok(())
})
}
fn get_signal_strength<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<u8>> + Send + 'a>> {
@@ -503,6 +532,11 @@ impl RigCat for SoapySdrRig {
bandwidth_hz: self.bandwidth_hz,
fir_taps: self.fir_taps,
cw_center_hz: 700,
sdr_gain_db: Some(
self.max_gain_db
.map(|max_gain| self.gain_db.min(max_gain))
.unwrap_or(self.gain_db),
),
wfm_deemphasis_us: self.wfm_deemphasis_us,
wfm_stereo: self.wfm_stereo,
wfm_stereo_detected,
@@ -173,6 +173,12 @@ impl IqSource for RealIqSource {
.map_err(|e| format!("Failed to retune SDR center frequency: {}", e))
}
fn set_gain(&mut self, gain_db: f64) -> Result<(), String> {
self.device
.set_gain(soapysdr::Direction::Rx, 0, gain_db)
.map_err(|e| format!("Failed to set SDR gain: {}", e))
}
fn handle_read_error(&mut self, err: &str) -> Result<bool, String> {
let err_lc = err.to_ascii_lowercase();
let is_overrun = err_lc.contains("overflow") || err_lc.contains("overrun");