[feat](trx-backend-soapysdr): add hardware AGC toggle and SDR settings UI row
Add hardware AGC on/off control for SoapySDR backend, wired through the full stack from RigCommand to the web UI: - RigCommand::SetSdrAgc(bool) + ClientCommand::SetSdrAgc in protocol - set_sdr_agc() on RigCat trait (not-supported default) - SoapySdrRig: agc_enabled field, set_sdr_agc() via pipeline agc_cmd, sdr_agc_enabled in filter_state(); removes the "not yet implemented" warning — gain_mode="auto" now properly enables hardware AGC via SoapySDR set_gain_mode() - IqSource::set_gain_mode() trait method; RealIqSource implements it - SdrPipeline: agc_cmd channel, read loop applies it each iteration - POST /set_sdr_agc endpoint in trx-frontend-http - New "SDR settings" full-row in index.html with Hardware AGC checkbox and RF Gain (moved out of WFM controls); row hidden when show_sdr_gain_control is false - app.js: AGC checkbox handler, disables RF gain input while AGC is on, syncs checkbox state from filter.sdr_agc_enabled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -563,6 +563,14 @@ async fn process_command(
|
||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||
return snapshot_from(ctx.state);
|
||||
}
|
||||
RigCommand::SetSdrAgc(enabled) => {
|
||||
if let Err(e) = ctx.rig.set_sdr_agc(enabled).await {
|
||||
return Err(RigError::communication(format!("set_sdr_agc: {e}")));
|
||||
}
|
||||
ctx.state.filter = ctx.rig.filter_state();
|
||||
let _ = ctx.state_tx.send(ctx.state.clone());
|
||||
return snapshot_from(ctx.state);
|
||||
}
|
||||
RigCommand::SetSdrSquelch {
|
||||
enabled,
|
||||
threshold_db,
|
||||
|
||||
@@ -57,6 +57,12 @@ pub trait IqSource: Send + 'static {
|
||||
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> {
|
||||
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.
|
||||
@@ -100,6 +106,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(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>>>,
|
||||
/// Current hardware center frequency in Hz, kept in sync by `SoapySdrRig`.
|
||||
/// Read by `SdrVirtualChannelManager` to validate and compute IF offsets.
|
||||
pub shared_center_hz: Arc<AtomicI64>,
|
||||
@@ -174,6 +183,8 @@ 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 agc_cmd: Arc<std::sync::Mutex<Option<bool>>> = Arc::new(std::sync::Mutex::new(None));
|
||||
let thread_agc_cmd = agc_cmd.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("sdr-iq-read".to_string())
|
||||
@@ -186,6 +197,7 @@ impl SdrPipeline {
|
||||
thread_spectrum_buf,
|
||||
thread_retune_cmd,
|
||||
thread_gain_cmd,
|
||||
thread_agc_cmd,
|
||||
);
|
||||
})
|
||||
.expect("failed to spawn sdr-iq-read thread");
|
||||
@@ -198,6 +210,7 @@ impl SdrPipeline {
|
||||
sdr_sample_rate,
|
||||
retune_cmd,
|
||||
gain_cmd,
|
||||
agc_cmd,
|
||||
shared_center_hz: Arc::new(AtomicI64::new(0)),
|
||||
audio_sample_rate,
|
||||
audio_channels: output_channels,
|
||||
@@ -277,6 +290,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>>>,
|
||||
agc_cmd: Arc<std::sync::Mutex<Option<bool>>>,
|
||||
) {
|
||||
let mut block = vec![Complex::new(0.0_f32, 0.0_f32); IQ_BLOCK_SIZE];
|
||||
let block_duration_ms = if sdr_sample_rate > 0 {
|
||||
@@ -309,6 +323,15 @@ fn iq_read_loop(
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(mut cmd) = agc_cmd.try_lock() {
|
||||
if let Some(enabled) = cmd.take() {
|
||||
if let Err(e) = source.set_gain_mode(enabled) {
|
||||
tracing::warn!("SDR AGC mode change to {} failed: {}", enabled, e);
|
||||
} else {
|
||||
tracing::info!("SDR AGC mode set to {}", enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n = match source.read_into(&mut block) {
|
||||
Ok(n) => {
|
||||
|
||||
@@ -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>,
|
||||
/// Whether hardware AGC is currently enabled.
|
||||
agc_enabled: bool,
|
||||
/// Whether software squelch is enabled on primary channel (except WFM mode).
|
||||
squelch_enabled: bool,
|
||||
/// Software squelch threshold (dBFS) on primary channel.
|
||||
@@ -133,13 +135,7 @@ impl SoapySdrRig {
|
||||
max_gain_db,
|
||||
);
|
||||
|
||||
if gain_mode == "auto" {
|
||||
tracing::warn!(
|
||||
"SoapySDR hardware AGC is not yet implemented; falling back to configured \
|
||||
gain of {} dB",
|
||||
gain_db,
|
||||
);
|
||||
}
|
||||
let agc_enabled = gain_mode == "auto";
|
||||
|
||||
let effective_gain_db = max_gain_db
|
||||
.map(|max_gain| gain_db.min(max_gain))
|
||||
@@ -289,6 +285,7 @@ impl SoapySdrRig {
|
||||
wfm_denoise: WfmDenoiseLevel::Auto,
|
||||
gain_db,
|
||||
max_gain_db,
|
||||
agc_enabled,
|
||||
squelch_enabled,
|
||||
squelch_threshold_db,
|
||||
ais_channel_indices: Some((primary_channel_count, primary_channel_count + 1)),
|
||||
@@ -582,6 +579,19 @@ impl RigCat for SoapySdrRig {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_sdr_agc<'a>(
|
||||
&'a mut self,
|
||||
enabled: bool,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
self.agc_enabled = enabled;
|
||||
if let Ok(mut cmd) = self.pipeline.agc_cmd.lock() {
|
||||
*cmd = Some(enabled);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_sdr_squelch<'a>(
|
||||
&'a mut self,
|
||||
enabled: bool,
|
||||
@@ -793,6 +803,7 @@ impl RigCat for SoapySdrRig {
|
||||
.map(|max_gain| self.gain_db.min(max_gain))
|
||||
.unwrap_or(self.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),
|
||||
wfm_deemphasis_us: self.wfm_deemphasis_us,
|
||||
|
||||
@@ -179,6 +179,12 @@ impl IqSource for RealIqSource {
|
||||
.map_err(|e| format!("Failed to set SDR gain: {}", e))
|
||||
}
|
||||
|
||||
fn set_gain_mode(&mut self, automatic: bool) -> Result<(), String> {
|
||||
self.device
|
||||
.set_gain_mode(soapysdr::Direction::Rx, 0, automatic)
|
||||
.map_err(|e| format!("Failed to set SDR gain mode: {}", e))
|
||||
}
|
||||
|
||||
fn handle_read_error(&mut self, err: &str, streak: u32) -> Result<bool, String> {
|
||||
const OVERFLOW_RESTART_STREAK: u32 = 50;
|
||||
const NON_OVERFLOW_RESTART_STREAK: u32 = 10;
|
||||
|
||||
Reference in New Issue
Block a user