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 dde995d..4787b74 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
@@ -173,6 +173,7 @@ function applyAuthRestrictions() {
const powerBtn = document.getElementById("power-btn");
const lockBtn = document.getElementById("lock-btn");
const freqInput = document.getElementById("freq");
+ const centerFreqInput = document.getElementById("center-freq");
const modeSelect = document.getElementById("mode");
const txLimitInput = document.getElementById("tx-limit");
const txLimitBtn = document.getElementById("tx-limit-btn");
@@ -193,6 +194,7 @@ function applyAuthRestrictions() {
// Disable frequency/mode inputs
if (freqInput) freqInput.disabled = true;
+ if (centerFreqInput) centerFreqInput.disabled = true;
if (modeSelect) modeSelect.disabled = true;
if (txLimitInput) txLimitInput.disabled = true;
@@ -260,18 +262,22 @@ function applyCapabilities(caps) {
// Spectrum panel (SDR-only)
const spectrumPanel = document.getElementById("spectrum-panel");
+ const centerFreqField = document.getElementById("center-freq-field");
if (spectrumPanel) {
if (caps.filter_controls) {
spectrumPanel.style.display = "";
+ if (centerFreqField) centerFreqField.style.display = "";
startSpectrumPolling();
} else {
spectrumPanel.style.display = "none";
+ if (centerFreqField) centerFreqField.style.display = "none";
stopSpectrumPolling();
}
}
}
const freqEl = document.getElementById("freq");
+const centerFreqEl = document.getElementById("center-freq");
const wavelengthEl = document.getElementById("wavelength");
const modeEl = document.getElementById("mode");
const bandLabel = document.getElementById("band-label");
@@ -316,6 +322,7 @@ let sigMeasureAccumMs = 0;
let sigMeasureWeighted = 0;
let sigMeasurePeak = null;
let lastFreqHz = null;
+let centerFreqDirty = false;
let jogStep = loadSetting("jogStep", 1000);
let minFreqStepHz = 1;
const VFO_COLORS = ["var(--accent-green)", "var(--accent-yellow)"];
@@ -619,6 +626,11 @@ function refreshFreqDisplay() {
refreshWavelengthDisplay(lastFreqHz);
}
+function refreshCenterFreqDisplay() {
+ if (!centerFreqEl || !lastSpectrumData || centerFreqDirty) return;
+ centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogStep);
+}
+
function parseFreqInput(val, defaultStep) {
if (!val) return null;
const trimmed = val.trim().toLowerCase();
@@ -701,6 +713,7 @@ function updateJogStepSupport(cap) {
});
refreshFreqDisplay();
+ refreshCenterFreqDisplay();
}
function normalizeMode(modeVal) {
@@ -751,7 +764,7 @@ function formatSignal(sUnits) {
}
function setDisabled(disabled) {
- [freqEl, modeEl, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
+ [freqEl, centerFreqEl, modeEl, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
if (el) el.disabled = disabled;
});
}
@@ -1299,6 +1312,32 @@ async function applyFreqFromInput() {
}
}
+async function applyCenterFreqFromInput() {
+ if (!centerFreqEl) return;
+ const parsedRaw = parseFreqInput(centerFreqEl.value, jogStep);
+ const parsed = alignFreqToRigStep(parsedRaw);
+ if (parsed === null) {
+ showHint("Central freq missing", 1500);
+ return;
+ }
+ if (!freqAllowed(parsed)) {
+ showHint("Out of supported bands", 1500);
+ return;
+ }
+ centerFreqDirty = false;
+ centerFreqEl.disabled = true;
+ showHint("Setting central frequency…");
+ try {
+ await postPath(`/set_center_freq?hz=${parsed}`);
+ showHint("Central freq set", 1500);
+ } catch (err) {
+ showHint("Set central freq failed", 2000);
+ console.error(err);
+ } finally {
+ centerFreqEl.disabled = false;
+ }
+}
+
freqEl.addEventListener("keydown", (e) => {
freqDirty = true;
if (e.key === "Enter") {
@@ -1306,6 +1345,15 @@ freqEl.addEventListener("keydown", (e) => {
applyFreqFromInput();
}
});
+if (centerFreqEl) {
+ centerFreqEl.addEventListener("keydown", (e) => {
+ centerFreqDirty = true;
+ if (e.key === "Enter") {
+ e.preventDefault();
+ applyCenterFreqFromInput();
+ }
+ });
+}
freqEl.addEventListener("wheel", (e) => {
e.preventDefault();
const direction = e.deltaY < 0 ? 1 : -1;
@@ -1394,6 +1442,7 @@ jogStepEl.addEventListener("click", (e) => {
btn.classList.add("active");
saveSetting("jogStep", jogStep);
refreshFreqDisplay();
+ refreshCenterFreqDisplay();
});
// Restore active jog step button from saved setting
@@ -2416,6 +2465,7 @@ async function fetchSpectrum() {
if (resp.status === 204) { lastSpectrumData = null; clearSpectrumCanvas(); return; }
if (!resp.ok) return;
lastSpectrumData = await resp.json();
+ refreshCenterFreqDisplay();
drawSpectrum(lastSpectrumData);
} catch (_) {}
}
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 ed46f22..69b0e62 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
@@ -84,6 +84,10 @@
Frequency
+
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 3fe76cf..dee39a7 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
@@ -239,6 +239,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
.inline { display: flex; gap: 0.5rem; align-items: center; }
.freq-inline {
align-items: flex-start;
+ flex-wrap: wrap;
}
.freq-field {
display: grid;
@@ -253,7 +254,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
flex: 1 1 auto;
min-width: 0;
}
-.frequency-col #freq {
+.frequency-col input.status-input {
width: 100%;
height: 3.35rem;
box-sizing: border-box;
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 54fabfc..6b1ed71 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
@@ -355,6 +355,14 @@ pub async fn set_freq(
send_command(&rig_tx, RigCommand::SetFreq(Freq { hz: query.hz })).await
}
+#[post("/set_center_freq")]
+pub async fn set_center_freq(
+ query: web::Query,
+ rig_tx: web::Data>,
+) -> Result {
+ send_command(&rig_tx, RigCommand::SetCenterFreq(Freq { hz: query.hz })).await
+}
+
#[derive(serde::Deserialize)]
pub struct ModeQuery {
pub mode: String,
@@ -633,6 +641,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(lock_panel)
.service(unlock_panel)
.service(set_freq)
+ .service(set_center_freq)
.service(set_mode)
.service(set_ptt)
.service(set_tx_limit)
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs
index 547a0c8..965d781 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs
@@ -591,6 +591,10 @@ mod tests {
#[test]
fn test_route_access_control_paths() {
assert_eq!(RouteAccess::from_path("/set_freq"), RouteAccess::Control);
+ assert_eq!(
+ RouteAccess::from_path("/set_center_freq"),
+ RouteAccess::Control
+ );
assert_eq!(RouteAccess::from_path("/set_mode"), RouteAccess::Control);
}
diff --git a/src/trx-core/src/rig/command.rs b/src/trx-core/src/rig/command.rs
index 875d431..360e8e4 100644
--- a/src/trx-core/src/rig/command.rs
+++ b/src/trx-core/src/rig/command.rs
@@ -10,6 +10,7 @@ use crate::RigMode;
pub enum RigCommand {
GetSnapshot,
SetFreq(Freq),
+ SetCenterFreq(Freq),
SetMode(RigMode),
SetPtt(bool),
PowerOn,
diff --git a/src/trx-core/src/rig/controller/handlers.rs b/src/trx-core/src/rig/controller/handlers.rs
index 677c64e..7e4bcab 100644
--- a/src/trx-core/src/rig/controller/handlers.rs
+++ b/src/trx-core/src/rig/controller/handlers.rs
@@ -491,6 +491,7 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box {
match cmd {
RigCommand::GetSnapshot => Box::new(GetSnapshotCommand),
RigCommand::SetFreq(freq) => Box::new(SetFreqCommand::new(freq)),
+ RigCommand::SetCenterFreq(_) => Box::new(GetSnapshotCommand),
RigCommand::SetMode(mode) => Box::new(SetModeCommand::new(mode)),
RigCommand::SetPtt(ptt) => Box::new(SetPttCommand::new(ptt)),
RigCommand::PowerOn => Box::new(PowerOnCommand),
diff --git a/src/trx-core/src/rig/mod.rs b/src/trx-core/src/rig/mod.rs
index 78791af..f15f281 100644
--- a/src/trx-core/src/rig/mod.rs
+++ b/src/trx-core/src/rig/mod.rs
@@ -88,6 +88,16 @@ pub trait RigCat: Rig + Send {
freq: Freq,
) -> Pin> + Send + 'a>>;
+ fn set_center_freq<'a>(
+ &'a mut self,
+ _freq: Freq,
+ ) -> Pin> + Send + 'a>> {
+ Box::pin(std::future::ready(Err(
+ Box::new(response::RigError::not_supported("set_center_freq"))
+ as Box,
+ )))
+ }
+
fn set_mode<'a>(
&'a mut self,
mode: RigMode,
diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs
index a83288e..f4c219d 100644
--- a/src/trx-protocol/src/mapping.rs
+++ b/src/trx-protocol/src/mapping.rs
@@ -21,6 +21,7 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand {
}
ClientCommand::GetState => RigCommand::GetSnapshot,
ClientCommand::SetFreq { freq_hz } => RigCommand::SetFreq(Freq { hz: freq_hz }),
+ ClientCommand::SetCenterFreq { freq_hz } => RigCommand::SetCenterFreq(Freq { hz: freq_hz }),
ClientCommand::SetMode { mode } => RigCommand::SetMode(parse_mode(&mode)),
ClientCommand::SetPtt { ptt } => RigCommand::SetPtt(ptt),
ClientCommand::PowerOn => RigCommand::PowerOn,
@@ -59,6 +60,7 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand {
match cmd {
RigCommand::GetSnapshot => ClientCommand::GetState,
RigCommand::SetFreq(freq) => ClientCommand::SetFreq { freq_hz: freq.hz },
+ RigCommand::SetCenterFreq(freq) => ClientCommand::SetCenterFreq { freq_hz: freq.hz },
RigCommand::SetMode(mode) => ClientCommand::SetMode {
mode: mode_to_string(&mode),
},
diff --git a/src/trx-protocol/src/types.rs b/src/trx-protocol/src/types.rs
index ca76e0a..eec9d52 100644
--- a/src/trx-protocol/src/types.rs
+++ b/src/trx-protocol/src/types.rs
@@ -15,6 +15,7 @@ pub enum ClientCommand {
GetState,
GetRigs,
SetFreq { freq_hz: u64 },
+ SetCenterFreq { freq_hz: u64 },
SetMode { mode: String },
SetPtt { ptt: bool },
PowerOn,
diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs
index 9def828..540268f 100644
--- a/src/trx-server/src/rig_task.rs
+++ b/src/trx-server/src/rig_task.rs
@@ -449,6 +449,13 @@ async fn process_command(
let _ = ctx.state_tx.send(ctx.state.clone());
return snapshot_from(ctx.state);
}
+ RigCommand::SetCenterFreq(freq) => {
+ if let Err(e) = ctx.rig.set_center_freq(freq).await {
+ return Err(RigError::communication(format!("set_center_freq: {e}")));
+ }
+ *ctx.poll_pause_until = Some(Instant::now() + Duration::from_millis(200));
+ return snapshot_from(ctx.state);
+ }
RigCommand::GetSpectrum => {
// Fetch current spectrum and embed it in a one-shot snapshot.
ctx.state.spectrum = ctx.rig.get_spectrum();
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 042012b..41025b7 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
@@ -33,6 +33,8 @@ pub struct SoapySdrRig {
/// How many Hz below the dial frequency the SDR hardware is actually tuned.
/// The DSP mixer compensates for this offset to demodulate the dial frequency.
center_offset_hz: i64,
+ /// Actual hardware center frequency currently tuned on the SDR.
+ center_hz: i64,
/// Used to send hardware retune commands to the IQ read loop.
retune_cmd: Arc>>,
}
@@ -94,14 +96,13 @@ impl SoapySdrRig {
let hardware_center_hz = initial_freq.hz as i64 - center_offset_hz;
// Create real IQ source from hardware device.
- let iq_source: Box =
- Box::new(real_iq_source::RealIqSource::new(
- args,
- hardware_center_hz as f64,
- sdr_sample_rate as f64,
- bandwidth_hz as f64,
- gain_db,
- )?);
+ let iq_source: Box = Box::new(real_iq_source::RealIqSource::new(
+ args,
+ hardware_center_hz as f64,
+ sdr_sample_rate as f64,
+ bandwidth_hz as f64,
+ gain_db,
+ )?);
let pipeline = dsp::SdrPipeline::start(
iq_source,
@@ -172,6 +173,7 @@ impl SoapySdrRig {
fir_taps,
spectrum_buf,
center_offset_hz,
+ center_hz: hardware_center_hz,
retune_cmd,
})
}
@@ -242,10 +244,34 @@ impl RigCat for SoapySdrRig {
Box::pin(async move {
tracing::debug!("SoapySdrRig: set_freq -> {} Hz", freq.hz);
self.freq = freq;
- // Retune the hardware center to keep the dial frequency off-DC.
- let hardware_hz = freq.hz as i64 - self.center_offset_hz;
+ let half_span_hz = i128::from(self.pipeline.sdr_sample_rate) / 2;
+ let current_center_hz = i128::from(self.center_hz);
+ let target_hz = i128::from(freq.hz);
+ let within_current_span = target_hz >= current_center_hz - half_span_hz
+ && target_hz <= current_center_hz + half_span_hz;
+
+ if !within_current_span {
+ // Only retune when the requested dial frequency leaves the
+ // currently captured SDR bandwidth.
+ let hardware_hz = freq.hz as i64 - self.center_offset_hz;
+ self.center_hz = hardware_hz;
+ if let Ok(mut cmd) = self.retune_cmd.lock() {
+ *cmd = Some(hardware_hz as f64);
+ }
+ }
+ Ok(())
+ })
+ }
+
+ fn set_center_freq<'a>(
+ &'a mut self,
+ freq: Freq,
+ ) -> Pin> + Send + 'a>> {
+ Box::pin(async move {
+ tracing::debug!("SoapySdrRig: set_center_freq -> {} Hz", freq.hz);
+ self.center_hz = freq.hz as i64;
if let Ok(mut cmd) = self.retune_cmd.lock() {
- *cmd = Some(hardware_hz as f64);
+ *cmd = Some(self.center_hz as f64);
}
Ok(())
})
@@ -402,11 +428,9 @@ impl RigCat for SoapySdrRig {
fn get_spectrum(&self) -> Option {
let bins = self.spectrum_buf.lock().ok()?.clone()?;
- // Report the actual hardware center frequency, not the dial frequency.
- let center_hz = (self.freq.hz as i64 - self.center_offset_hz) as u64;
Some(SpectrumData {
bins,
- center_hz,
+ center_hz: self.center_hz.max(0) as u64,
sample_rate: self.pipeline.sdr_sample_rate,
})
}