diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
index 31f059a..4269b59 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
@@ -1257,6 +1257,13 @@ function render(update) {
if (update.filter && typeof update.filter.bandwidth_hz === "number") {
currentBandwidthHz = update.filter.bandwidth_hz;
syncBandwidthInput(currentBandwidthHz);
+ if (
+ sdrGainEl
+ && typeof update.filter.sdr_gain_db === "number"
+ && document.activeElement !== sdrGainEl
+ ) {
+ sdrGainEl.value = String(Math.round(update.filter.sdr_gain_db));
+ }
if (wfmDeemphasisEl && typeof update.filter.wfm_deemphasis_us === "number") {
wfmDeemphasisEl.value = String(update.filter.wfm_deemphasis_us);
}
@@ -2567,6 +2574,8 @@ const audioRow = document.getElementById("audio-row");
const wfmControlsCol = document.getElementById("wfm-controls-col");
const wfmDeemphasisEl = document.getElementById("wfm-deemphasis");
const wfmAudioModeEl = document.getElementById("wfm-audio-mode");
+const sdrGainEl = document.getElementById("sdr-gain-db");
+const sdrGainSetBtn = document.getElementById("sdr-gain-set");
const wfmStFlagEl = document.getElementById("wfm-st-flag");
// Hide audio row if audio is not configured on the server
@@ -2611,6 +2620,23 @@ if (wfmDeemphasisEl) {
postPath(`/set_wfm_deemphasis?us=${encodeURIComponent(wfmDeemphasisEl.value)}`).catch(() => {});
});
}
+function submitSdrGain() {
+ if (!sdrGainEl) return;
+ const parsed = Number.parseFloat(sdrGainEl.value);
+ if (!Number.isFinite(parsed) || parsed < 0) return;
+ postPath(`/set_sdr_gain?db=${encodeURIComponent(parsed)}`).catch(() => {});
+}
+if (sdrGainSetBtn) {
+ sdrGainSetBtn.addEventListener("click", submitSdrGain);
+}
+if (sdrGainEl) {
+ sdrGainEl.addEventListener("keydown", (ev) => {
+ if (ev.key === "Enter") {
+ ev.preventDefault();
+ submitSdrGain();
+ }
+ });
+}
function updateWfmControls() {
if (!wfmControlsCol) return;
const mode = (modeEl && modeEl.value ? modeEl.value : "").toUpperCase();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index c58f93a..08fe3d9 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -165,6 +165,10 @@
+
+
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index 957618d..b8f3bf7 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -186,6 +186,9 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
width: auto;
font-size: 0.9rem;
}
+.wfm-control input.status-input {
+ width: 5rem;
+}
.wfm-control button.status-input {
padding: 0.45rem 0.5rem;
height: auto;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs
index 67dc8f1..28e7c14 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs
@@ -473,6 +473,19 @@ pub async fn set_fir_taps(
send_command(&rig_tx, RigCommand::SetFirTaps(query.taps)).await
}
+#[derive(serde::Deserialize)]
+pub struct SdrGainQuery {
+ pub db: f64,
+}
+
+#[post("/set_sdr_gain")]
+pub async fn set_sdr_gain(
+ query: web::Query,
+ rig_tx: web::Data>,
+) -> Result {
+ send_command(&rig_tx, RigCommand::SetSdrGain(query.db)).await
+}
+
#[derive(serde::Deserialize)]
pub struct WfmDeemphasisQuery {
pub us: u32,
@@ -710,6 +723,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(set_tx_limit)
.service(set_bandwidth)
.service(set_fir_taps)
+ .service(set_sdr_gain)
.service(set_wfm_deemphasis)
.service(set_wfm_stereo)
.service(toggle_aprs_decode)
diff --git a/src/trx-core/src/rig/command.rs b/src/trx-core/src/rig/command.rs
index bc04636..3284a84 100644
--- a/src/trx-core/src/rig/command.rs
+++ b/src/trx-core/src/rig/command.rs
@@ -33,6 +33,7 @@ pub enum RigCommand {
ResetWsprDecoder,
SetBandwidth(u32),
SetFirTaps(u32),
+ SetSdrGain(f64),
SetWfmDeemphasis(u32),
SetWfmStereo(bool),
GetSpectrum,
diff --git a/src/trx-core/src/rig/controller/handlers.rs b/src/trx-core/src/rig/controller/handlers.rs
index 8d5e42b..c505f7f 100644
--- a/src/trx-core/src/rig/controller/handlers.rs
+++ b/src/trx-core/src/rig/controller/handlers.rs
@@ -516,6 +516,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box {
| RigCommand::ResetWsprDecoder
| RigCommand::SetBandwidth(_)
| RigCommand::SetFirTaps(_)
+ | RigCommand::SetSdrGain(_)
| RigCommand::SetWfmDeemphasis(_)
| RigCommand::SetWfmStereo(_)
| RigCommand::GetSpectrum => Box::new(GetSnapshotCommand),
diff --git a/src/trx-core/src/rig/mod.rs b/src/trx-core/src/rig/mod.rs
index 7eb675f..ffe7004 100644
--- a/src/trx-core/src/rig/mod.rs
+++ b/src/trx-core/src/rig/mod.rs
@@ -165,6 +165,16 @@ pub trait RigCat: Rig + Send {
)))
}
+ fn set_sdr_gain<'a>(
+ &'a mut self,
+ _gain_db: f64,
+ ) -> Pin> + Send + 'a>> {
+ Box::pin(std::future::ready(Err(
+ Box::new(response::RigError::not_supported("set_sdr_gain"))
+ as Box,
+ )))
+ }
+
fn set_wfm_stereo<'a>(
&'a mut self,
_enabled: bool,
diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs
index cc6fa46..dd8f59d 100644
--- a/src/trx-core/src/rig/state.rs
+++ b/src/trx-core/src/rig/state.rs
@@ -269,6 +269,8 @@ pub struct RigFilterState {
pub bandwidth_hz: u32,
pub fir_taps: u32,
pub cw_center_hz: u32,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub sdr_gain_db: Option,
#[serde(default = "default_wfm_deemphasis_us")]
pub wfm_deemphasis_us: u32,
#[serde(default = "default_wfm_stereo")]
diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs
index c2214e3..e5e7227 100644
--- a/src/trx-protocol/src/codec.rs
+++ b/src/trx-protocol/src/codec.rs
@@ -297,6 +297,7 @@ mod tests {
bandwidth_hz: 3000,
fir_taps: 64,
cw_center_hz: 700,
+ sdr_gain_db: Some(12.0),
wfm_deemphasis_us: 75,
wfm_stereo: true,
wfm_stereo_detected: false,
@@ -335,6 +336,7 @@ mod tests {
bandwidth_hz: 12000,
fir_taps: 128,
cw_center_hz: 700,
+ sdr_gain_db: Some(18.0),
wfm_deemphasis_us: 50,
wfm_stereo: true,
wfm_stereo_detected: true,
@@ -346,6 +348,7 @@ mod tests {
let f = decoded.filter.expect("filter should round-trip");
assert_eq!(f.bandwidth_hz, 12000);
assert_eq!(f.fir_taps, 128);
+ assert_eq!(f.sdr_gain_db, Some(18.0));
assert_eq!(f.wfm_deemphasis_us, 50);
assert!(f.wfm_stereo_detected);
}
diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs
index 5ae4530..3ccac76 100644
--- a/src/trx-protocol/src/mapping.rs
+++ b/src/trx-protocol/src/mapping.rs
@@ -48,6 +48,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand {
ClientCommand::ResetWsprDecoder => RigCommand::ResetWsprDecoder,
ClientCommand::SetBandwidth { bandwidth_hz } => RigCommand::SetBandwidth(bandwidth_hz),
ClientCommand::SetFirTaps { taps } => RigCommand::SetFirTaps(taps),
+ ClientCommand::SetSdrGain { gain_db } => RigCommand::SetSdrGain(gain_db),
ClientCommand::SetWfmDeemphasis { deemphasis_us } => {
RigCommand::SetWfmDeemphasis(deemphasis_us)
}
@@ -93,6 +94,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
RigCommand::ResetWsprDecoder => ClientCommand::ResetWsprDecoder,
RigCommand::SetBandwidth(bandwidth_hz) => ClientCommand::SetBandwidth { bandwidth_hz },
RigCommand::SetFirTaps(taps) => ClientCommand::SetFirTaps { taps },
+ RigCommand::SetSdrGain(gain_db) => ClientCommand::SetSdrGain { gain_db },
RigCommand::SetWfmDeemphasis(deemphasis_us) => {
ClientCommand::SetWfmDeemphasis { deemphasis_us }
}
diff --git a/src/trx-protocol/src/types.rs b/src/trx-protocol/src/types.rs
index f118b56..b4804e0 100644
--- a/src/trx-protocol/src/types.rs
+++ b/src/trx-protocol/src/types.rs
@@ -38,6 +38,7 @@ pub enum ClientCommand {
ResetWsprDecoder,
SetBandwidth { bandwidth_hz: u32 },
SetFirTaps { taps: u32 },
+ SetSdrGain { gain_db: f64 },
SetWfmDeemphasis { deemphasis_us: u32 },
SetWfmStereo { enabled: bool },
GetSpectrum,
diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs
index 16da8fd..2edab59 100644
--- a/src/trx-server/src/rig_task.rs
+++ b/src/trx-server/src/rig_task.rs
@@ -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}")));
diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs
index a8102ed..0077531 100644
--- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs
+++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs
@@ -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>>,
+ /// 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>>,
}
impl SdrPipeline {
@@ -785,6 +794,8 @@ impl SdrPipeline {
let thread_spectrum_buf = spectrum_buf.clone();
let retune_cmd: Arc>> = Arc::new(std::sync::Mutex::new(None));
let thread_retune_cmd = retune_cmd.clone();
+ let gain_cmd: Arc>> = 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>>,
spectrum_buf: Arc>>>,
retune_cmd: Arc>>,
+ gain_cmd: Arc>>,
) {
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) => {
diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs
index 9981df9..e4ab201 100644
--- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs
+++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs
@@ -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,
}
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> + 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> + 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,
diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs
index 6fabd20..3fc123d 100644
--- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs
+++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs
@@ -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 {
let err_lc = err.to_ascii_lowercase();
let is_overrun = err_lc.contains("overflow") || err_lc.contains("overrun");