From d468a9644810dc68ab9cad263ec3169f75800ae8 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Fri, 27 Mar 2026 18:52:12 +0100 Subject: [PATCH] [fix](trx-backend-soapysdr): stabilize WFM signal strength and speed up SDR polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smooth envelope power (I²+Q²) instead of filtering I/Q components separately — eliminates ~6 dB modulation-dependent fluctuation caused by FM carrier rotation in the IQ plane. Reset signal strength on frequency change. Reduce SDR poll interval from 500ms to 100ms. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Stan Grams --- src/trx-server/src/main.rs | 4 ++ .../trx-backend-soapysdr/src/dsp/channel.rs | 37 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index c42c119..08229bf 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -1049,6 +1049,10 @@ async fn main() -> DynResult<()> { ); if let Some(prebuilt) = sdr_prebuilt_rig { task_config.prebuilt_rig = Some(prebuilt); + // SDR signal strength is a pre-computed field read — no serial + // round-trip — so we can poll much faster than the CAT default. + task_config.polling = + AdaptivePolling::new(Duration::from_millis(100), Duration::from_millis(100)); } // Spawn rig task. diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs index 9c8b515..c4698ec 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs @@ -297,18 +297,15 @@ pub struct ChannelDsp { squelch: VirtualSquelch, noise_blanker: NoiseBlanker, last_signal_db: f32, - /// Single-pole IIR states for narrow IQ lowpass (WFM carrier measurement). - /// Filtering the IQ signal (not the power) rejects out-of-band noise so the - /// meter reads carrier level, not total wideband noise power. + /// Single-pole IIR state for smoothed envelope power (WFM signal strength). carrier_iq_i: f32, - carrier_iq_q: f32, - /// IIR coefficient for the narrow IQ carrier filter, precomputed from sample rate. + /// IIR coefficient for the envelope power smoother, precomputed from sample rate. carrier_iq_alpha: f32, } impl ChannelDsp { - /// Compute the single-pole IIR alpha for narrow IQ carrier measurement. - /// Uses ~500 Hz cutoff so the meter reads carrier level, not wideband noise. + /// Compute the single-pole IIR alpha for envelope power smoothing. + /// Uses ~500 Hz cutoff for a responsive but stable S-meter reading. fn narrow_carrier_alpha(channel_sample_rate: u32) -> f32 { const CARRIER_BW_HZ: f32 = 500.0; if channel_sample_rate == 0 { @@ -333,6 +330,10 @@ impl ChannelDsp { } else { 2.0 * std::f64::consts::PI * channel_if_hz / self.sdr_sample_rate as f64 }; + // Reset signal strength so the meter doesn't show a stale reading + // from the previous frequency while the IIR catches up. + self.carrier_iq_i = 0.0; + self.last_signal_db = -120.0; } fn pipeline_rates( @@ -427,7 +428,6 @@ impl ChannelDsp { self.audio_dc = dc_for_mode(&self.mode); self.carrier_iq_alpha = Self::narrow_carrier_alpha(channel_sample_rate); self.carrier_iq_i = 0.0; - self.carrier_iq_q = 0.0; self.frame_buf.clear(); self.frame_buf_offset = 0; } @@ -544,7 +544,6 @@ impl ChannelDsp { noise_blanker: NoiseBlanker::new(nb_cfg.enabled, nb_cfg.threshold), last_signal_db: -120.0, carrier_iq_i: 0.0, - carrier_iq_q: 0.0, carrier_iq_alpha: Self::narrow_carrier_alpha(channel_sample_rate), } } @@ -760,18 +759,17 @@ impl ChannelDsp { // Signal strength measurement (before AGC). { if self.mode == RigMode::WFM { - // WFM: narrow-band carrier measurement via IQ-domain lowpass. - // A single-pole IIR on each of I and Q (≈500 Hz cutoff) rejects - // wideband noise *before* computing power, so the meter reads - // carrier level rather than total noise across the 180 kHz channel. + // WFM: smooth envelope power directly. + // FM is constant-envelope, so I²+Q² is inherently stable + // regardless of modulation content. Averaging power (not I/Q + // components) avoids the ~6 dB dip that occurs when modulation + // rotates the carrier away from DC in the IQ plane. let alpha = self.carrier_iq_alpha; for s in decimated.iter() { - self.carrier_iq_i += alpha * (s.re - self.carrier_iq_i); - self.carrier_iq_q += alpha * (s.im - self.carrier_iq_q); + let pwr = s.re * s.re + s.im * s.im; + self.carrier_iq_i += alpha * (pwr - self.carrier_iq_i); } - let carrier_pwr = - self.carrier_iq_i * self.carrier_iq_i + self.carrier_iq_q * self.carrier_iq_q; - self.last_signal_db = 10.0 * carrier_pwr.max(1e-12).log10(); + self.last_signal_db = 10.0 * self.carrier_iq_i.max(1e-12).log10(); } else { // Other modes: peak IQ magnitude with EMA smoothing. const SIGNAL_EMA_ALPHA: f32 = 0.4; @@ -780,8 +778,7 @@ impl ChannelDsp { .map(|s| s.re * s.re + s.im * s.im) .fold(0.0_f32, f32::max); let peak_db = 10.0 * peak_power.max(1e-12).log10(); - self.last_signal_db += - SIGNAL_EMA_ALPHA * (peak_db - self.last_signal_db); + self.last_signal_db += SIGNAL_EMA_ALPHA * (peak_db - self.last_signal_db); } }