[fix](trx-backend-soapysdr): keep vchans across scheduler retunes

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-13 07:38:56 +01:00
parent c014f5cdc2
commit 73173d29ff
@@ -18,8 +18,9 @@
//! ## Center-frequency updates //! ## Center-frequency updates
//! //!
//! When the hardware retunes (changing `center_hz`), all channel IF offsets must //! When the hardware retunes (changing `center_hz`), all channel IF offsets must
//! be recomputed. The rig calls `update_center_hz()` after every retune; this //! be recomputed. The rig calls `update_center_hz()` after every retune; this
//! updates every `ChannelDsp` in a single write-locked pass. //! updates every `ChannelDsp` in place and pauses out-of-span channels instead
//! of destroying them.
use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@@ -90,7 +91,7 @@ pub struct SdrVirtualChannelManager {
/// Maximum total channels including the primary (enforced on `add_channel`). /// Maximum total channels including the primary (enforced on `add_channel`).
max_total: usize, max_total: usize,
channels: RwLock<Vec<ManagedChannel>>, channels: RwLock<Vec<ManagedChannel>>,
/// Fires whenever a channel is destroyed (e.g. went out of SDR bandwidth). /// Fires whenever a channel is explicitly destroyed.
destroyed_tx: broadcast::Sender<Uuid>, destroyed_tx: broadcast::Sender<Uuid>,
} }
@@ -206,31 +207,24 @@ impl SdrVirtualChannelManager {
} }
/// Called by `SoapySdrRig` whenever the hardware center frequency changes. /// Called by `SoapySdrRig` whenever the hardware center frequency changes.
/// Recomputes the IF offset for every virtual channel. /// Recomputes the IF offset for every virtual channel and pauses any
/// channel that is temporarily outside the current SDR span.
pub fn update_center_hz(&self, new_center_hz: i64) { pub fn update_center_hz(&self, new_center_hz: i64) {
self.center_hz.store(new_center_hz, Ordering::Relaxed); self.center_hz.store(new_center_hz, Ordering::Relaxed);
let half_span = self.half_span_hz(); let half_span = self.half_span_hz();
// Single pass under read lock: update in-band IF offsets and collect OOB IDs. let channels = self.channels.read().unwrap();
let oob_ids: Vec<Uuid> = { let dsps = self.pipeline.channel_dsps.read().unwrap();
let channels = self.channels.read().unwrap(); for ch in channels.iter().filter(|c| !c.permanent) {
let dsps = self.pipeline.channel_dsps.read().unwrap(); let new_if_hz = ch.freq_hz as i64 - new_center_hz;
let mut oob = Vec::new(); let in_span = new_if_hz.unsigned_abs() as i64 <= half_span;
for ch in channels.iter().filter(|c| !c.permanent) { if let Some(dsp_arc) = dsps.get(ch.pipeline_slot) {
let new_if_hz = ch.freq_hz as i64 - new_center_hz; let mut dsp = dsp_arc.lock().unwrap();
if new_if_hz.unsigned_abs() as i64 > half_span { if in_span {
oob.push(ch.id); dsp.set_channel_if_hz(new_if_hz as f64);
} else if let Some(dsp_arc) = dsps.get(ch.pipeline_slot) {
dsp_arc.lock().unwrap().set_channel_if_hz(new_if_hz as f64);
} }
dsp.set_processing_enabled(in_span);
} }
oob
}; // read locks released here
// Destroy OOB channels and notify subscribers.
for id in oob_ids {
let _ = self.remove_channel(id); // acquires write lock internally
let _ = self.destroyed_tx.send(id);
} }
} }
@@ -508,4 +502,24 @@ mod tests {
assert_eq!(visible.len(), 2); assert_eq!(visible.len(), 2);
assert!(visible.iter().all(|channel| channel.id != hidden_id)); assert!(visible.iter().all(|channel| channel.id != hidden_id));
} }
#[test]
fn retune_keeps_virtual_channel_allocated() {
let p = make_pipeline();
let mgr = SdrVirtualChannelManager::new(p, 1, 4);
mgr.update_center_hz(14_100_000);
let mut destroyed_rx = mgr.subscribe_destroyed();
let (id, _) = mgr.add_channel(14_074_000, &RigMode::USB).unwrap();
mgr.update_center_hz(16_000_000);
assert!(mgr.channels().iter().any(|channel| channel.id == id));
assert!(matches!(
destroyed_rx.try_recv(),
Err(tokio::sync::broadcast::error::TryRecvError::Empty)
));
mgr.update_center_hz(14_100_000);
assert!(mgr.channels().iter().any(|channel| channel.id == id));
}
} }