[fix](trx-backend-soapysdr): ensure IQ stream recovery after overflow

Two problems prevented reliable recovery after a persistent overflow:

1. Restart storm: once read_error_streak >= 3, every subsequent read
   failure triggered a deactivate/activate cycle, potentially preventing
   the hardware from stabilising. Fix: after a successful restart, reset
   read_error_streak to 1 so the stream gets 2 more failures before the
   next restart attempt.

2. Stuck-deactivated stream: if activate() failed after overflow, the
   stream was left deactivated. Subsequent reads returned non-overflow
   errors which handle_read_error ignored (Ok(false)), so the stream was
   never reactivated. Fix: add a high-streak fallback (>= 10 consecutive
   errors of any kind) that also attempts a full deactivate/activate
   restart, covering the stuck-deactivated case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-07 09:31:17 +01:00
parent bfc510e1eb
commit 1001786a08
2 changed files with 31 additions and 12 deletions
@@ -246,6 +246,7 @@ fn iq_read_loop(
read_error_streak = read_error_streak.saturating_add(1);
let err_lc = e.to_ascii_lowercase();
let is_overflow = err_lc.contains("overflow") || err_lc.contains("overrun");
let restarted = read_error_streak >= 3;
let recovered = match source.handle_read_error(&e, read_error_streak) {
Ok(result) => result,
Err(recovery_err) => {
@@ -256,6 +257,12 @@ fn iq_read_loop(
false
}
};
// After a stream restart, reset the streak so we don't
// immediately trigger another restart on the first transient
// failure during hardware stabilisation.
if restarted && recovered {
read_error_streak = 1;
}
if is_overflow {
let now = Instant::now();
if overflow_log_window_start
@@ -182,23 +182,35 @@ impl IqSource for RealIqSource {
fn handle_read_error(&mut self, err: &str, streak: u32) -> Result<bool, String> {
let err_lc = err.to_ascii_lowercase();
let is_overrun = err_lc.contains("overflow") || err_lc.contains("overrun");
if !is_overrun {
if is_overrun {
// Overflow is often transient; avoid immediate stream restart churn.
// Only restart after several consecutive overflow failures.
if streak < 3 {
return Ok(true);
}
tracing::warn!(
"SoapySDR RX overflow persists (streak={}); restarting RX stream",
streak
);
} else if streak >= 10 {
// Non-overflow errors at a high streak (e.g. reads on a
// deactivated stream after a failed activate) — attempt a
// full restart to recover.
tracing::warn!(
"SoapySDR RX persistent error (streak={}): {}; restarting RX stream",
streak,
err
);
} else {
return Ok(false);
}
// Overflow is often transient; avoid immediate stream restart churn.
// Only restart after several consecutive read failures.
if streak < 3 {
return Ok(true);
}
tracing::warn!(
"SoapySDR RX overflow persists (streak={}); restarting RX stream",
streak
);
let _ = self.stream.deactivate(None);
std::thread::sleep(std::time::Duration::from_millis(25));
std::thread::sleep(std::time::Duration::from_millis(50));
self.stream
.activate(None)
.map_err(|e| format!("Failed to reactivate RX stream after overflow: {}", e))?;
.map_err(|e| format!("Failed to reactivate RX stream: {}", e))?;
Ok(true)
}
}