[fix](trx-core): expose rig min tuning step and align UI tuning

Add min_freq_step_hz to RigCapabilities, set backend values, and make HTTP frontend parse suffix-less frequency input using the selected unit while snapping set/jog frequencies to rig step granularity.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-13 00:17:16 +01:00
parent a7719bd7a9
commit 81890b15a8
11 changed files with 87 additions and 12 deletions
+1
View File
@@ -371,6 +371,7 @@ mod tests {
model: "Dummy".to_string(), model: "Dummy".to_string(),
revision: "1".to_string(), revision: "1".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![Band { supported_bands: vec![Band {
low_hz: 7_000_000, low_hz: 7_000_000,
high_hz: 7_200_000, high_hz: 7_200_000,
@@ -294,6 +294,7 @@ mod tests {
model: "Dummy".to_string(), model: "Dummy".to_string(),
revision: "1".to_string(), revision: "1".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![Band { supported_bands: vec![Band {
low_hz: 14_000_000, low_hz: 14_000_000,
high_hz: 14_350_000, high_hz: 14_350_000,
@@ -45,6 +45,7 @@ let sigMeasuring = false;
let sigSamples = []; let sigSamples = [];
let lastFreqHz = null; let lastFreqHz = null;
let jogStep = loadSetting("jogStep", 1000); let jogStep = loadSetting("jogStep", 1000);
let minFreqStepHz = 1;
const VFO_COLORS = ["var(--accent-green)", "var(--accent-yellow)"]; const VFO_COLORS = ["var(--accent-green)", "var(--accent-yellow)"];
function vfoColor(idx) { function vfoColor(idx) {
if (idx < VFO_COLORS.length) return VFO_COLORS[idx]; if (idx < VFO_COLORS.length) return VFO_COLORS[idx];
@@ -89,9 +90,9 @@ function formatFreq(hz) {
function formatFreqForStep(hz, step) { function formatFreqForStep(hz, step) {
if (!Number.isFinite(hz)) return "--"; if (!Number.isFinite(hz)) return "--";
if (step === 1_000_000) return (hz / 1_000_000).toFixed(6); if (step >= 1_000_000) return (hz / 1_000_000).toFixed(6);
if (step === 1_000) return (hz / 1_000).toFixed(3); if (step >= 1_000) return (hz / 1_000).toFixed(3);
if (step === 1) return String(Math.round(hz)); if (step >= 1) return String(Math.round(hz));
return formatFreq(hz); return formatFreq(hz);
} }
@@ -116,11 +117,11 @@ function parseFreqInput(val, defaultStep) {
num *= 1_000; num *= 1_000;
} else if (!unit) { } else if (!unit) {
// Use currently selected input unit when user omits suffix. // Use currently selected input unit when user omits suffix.
if (defaultStep === 1_000_000) { if (defaultStep >= 1_000_000) {
num *= 1_000_000; num *= 1_000_000;
} else if (defaultStep === 1_000) { } else if (defaultStep >= 1_000) {
num *= 1_000; num *= 1_000;
} else if (defaultStep === 1) { } else if (defaultStep >= 1) {
// already Hz // already Hz
} else { } else {
// Fallback heuristic. // Fallback heuristic.
@@ -136,6 +137,54 @@ function parseFreqInput(val, defaultStep) {
return Math.round(num); return Math.round(num);
} }
function normalizeMinFreqStep(cap) {
const val = Number(cap && cap.min_freq_step_hz);
if (!Number.isFinite(val) || val < 1) return 1;
return Math.round(val);
}
function alignFreqToRigStep(hz) {
if (!Number.isFinite(hz)) return hz;
const step = Math.max(1, minFreqStepHz);
return Math.round(hz / step) * step;
}
function updateJogStepSupport(cap) {
const nextMinStep = normalizeMinFreqStep(cap);
minFreqStepHz = nextMinStep;
const stepRoot = document.getElementById("jog-step");
if (!stepRoot) return;
const buttons = Array.from(stepRoot.querySelectorAll("button[data-step]"));
if (buttons.length === 0) return;
buttons.forEach((btn) => {
const base = Number(btn.dataset.baseStep || btn.dataset.step);
if (Number.isFinite(base) && base > 0) {
btn.dataset.baseStep = String(Math.round(base));
btn.dataset.step = String(Math.max(Math.round(base), minFreqStepHz));
}
});
const steps = buttons
.map((btn) => Number(btn.dataset.step))
.filter((s) => Number.isFinite(s) && s > 0);
if (steps.length === 0) return;
const current = Number(jogStep);
const desired =
Number.isFinite(current) && current >= minFreqStepHz ? current : Math.max(steps[0], minFreqStepHz);
jogStep = steps.reduce((best, s) => (Math.abs(s - desired) < Math.abs(best - desired) ? s : best), steps[0]);
saveSetting("jogStep", jogStep);
buttons.forEach((btn) => {
btn.classList.toggle("active", Number(btn.dataset.step) === jogStep);
});
refreshFreqDisplay();
}
function normalizeMode(modeVal) { function normalizeMode(modeVal) {
if (typeof modeVal === "string") return modeVal; if (typeof modeVal === "string") return modeVal;
if (modeVal && typeof modeVal === "object") { if (modeVal && typeof modeVal === "object") {
@@ -256,6 +305,7 @@ function render(update) {
} }
} }
if (update.info && update.info.capabilities) { if (update.info && update.info.capabilities) {
updateJogStepSupport(update.info.capabilities);
updateSupportedBands(update.info.capabilities); updateSupportedBands(update.info.capabilities);
} }
if (update.status && update.status.freq && typeof update.status.freq.hz === "number") { if (update.status && update.status.freq && typeof update.status.freq.hz === "number") {
@@ -572,7 +622,8 @@ pttBtn.addEventListener("click", async () => {
}); });
freqBtn.addEventListener("click", async () => { freqBtn.addEventListener("click", async () => {
const parsed = parseFreqInput(freqEl.value, jogStep); const parsedRaw = parseFreqInput(freqEl.value, jogStep);
const parsed = alignFreqToRigStep(parsedRaw);
if (parsed === null) { if (parsed === null) {
showHint("Freq missing", 1500); showHint("Freq missing", 1500);
return; return;
@@ -612,7 +663,7 @@ const jogStepEl = document.getElementById("jog-step");
async function jogFreq(direction) { async function jogFreq(direction) {
if (lastLocked) { showHint("Locked", 1500); return; } if (lastLocked) { showHint("Locked", 1500); return; }
if (lastFreqHz === null) return; if (lastFreqHz === null) return;
const newHz = lastFreqHz + direction * jogStep; const newHz = alignFreqToRigStep(lastFreqHz + direction * jogStep);
if (!freqAllowed(newHz)) { if (!freqAllowed(newHz)) {
showHint("Out of supported bands", 1500); showHint("Out of supported bands", 1500);
return; return;
@@ -679,7 +730,7 @@ window.addEventListener("mouseup", () => {
jogStepEl.addEventListener("click", (e) => { jogStepEl.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-step]"); const btn = e.target.closest("button[data-step]");
if (!btn) return; if (!btn) return;
jogStep = parseInt(btn.dataset.step, 10); jogStep = Math.max(parseInt(btn.dataset.step, 10), minFreqStepHz);
jogStepEl.querySelectorAll("button").forEach((b) => b.classList.remove("active")); jogStepEl.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
btn.classList.add("active"); btn.classList.add("active");
saveSetting("jogStep", jogStep); saveSetting("jogStep", jogStep);
@@ -687,9 +738,17 @@ jogStepEl.addEventListener("click", (e) => {
}); });
// Restore active jog step button from saved setting // Restore active jog step button from saved setting
jogStepEl.querySelectorAll("button").forEach((b) => { {
b.classList.toggle("active", parseInt(b.dataset.step, 10) === jogStep); const buttons = Array.from(jogStepEl.querySelectorAll("button[data-step]"));
}); const active =
buttons.find((b) => parseInt(b.dataset.step, 10) === jogStep) ||
buttons.find((b) => parseInt(b.dataset.step, 10) === 1000) ||
buttons[0];
if (active) {
jogStep = parseInt(active.dataset.step, 10);
buttons.forEach((b) => b.classList.toggle("active", b === active));
}
}
modeBtn.addEventListener("click", async () => { modeBtn.addEventListener("click", async () => {
const mode = modeEl.value || ""; const mode = modeEl.value || "";
@@ -614,6 +614,7 @@ impl From<RigInfoPlaceholder> for RigInfo {
model: "Rig".to_string(), model: "Rig".to_string(),
revision: "".to_string(), revision: "".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![], supported_bands: vec![],
supported_modes: vec![], supported_modes: vec![],
num_vfos: 0, num_vfos: 0,
@@ -542,6 +542,7 @@ mod tests {
model: "Mock".to_string(), model: "Mock".to_string(),
revision: "1.0".to_string(), revision: "1.0".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![], supported_bands: vec![],
supported_modes: vec![], supported_modes: vec![],
num_vfos: 2, num_vfos: 2,
@@ -595,6 +596,7 @@ mod tests {
model: "Mock".to_string(), model: "Mock".to_string(),
revision: "1.0".to_string(), revision: "1.0".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![], supported_bands: vec![],
supported_modes: vec![], supported_modes: vec![],
num_vfos: 2, num_vfos: 2,
@@ -439,6 +439,7 @@ mod tests {
model: "Mock".to_string(), model: "Mock".to_string(),
revision: "1.0".to_string(), revision: "1.0".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![], supported_bands: vec![],
supported_modes: vec![], supported_modes: vec![],
num_vfos: 2, num_vfos: 2,
+6
View File
@@ -39,6 +39,8 @@ pub struct RigInfo {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RigCapabilities { pub struct RigCapabilities {
#[serde(default = "default_min_freq_step_hz")]
pub min_freq_step_hz: u64,
pub supported_bands: Vec<Band>, pub supported_bands: Vec<Band>,
pub supported_modes: Vec<RigMode>, pub supported_modes: Vec<RigMode>,
pub num_vfos: usize, pub num_vfos: usize,
@@ -51,6 +53,10 @@ pub struct RigCapabilities {
pub split: bool, pub split: bool,
} }
fn default_min_freq_step_hz() -> u64 {
1
}
/// Common interface for rig backends. /// Common interface for rig backends.
pub trait Rig { pub trait Rig {
fn info(&self) -> &RigInfo; fn info(&self) -> &RigInfo;
+1
View File
@@ -344,6 +344,7 @@ mod tests {
model: "Dummy".to_string(), model: "Dummy".to_string(),
revision: "1".to_string(), revision: "1".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![Band { supported_bands: vec![Band {
low_hz: 7_000_000, low_hz: 7_000_000,
high_hz: 7_200_000, high_hz: 7_200_000,
+1
View File
@@ -37,6 +37,7 @@ impl DummyRig {
model: "dummy".to_string(), model: "dummy".to_string(),
revision: "1.0".to_string(), revision: "1.0".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 1,
supported_bands: vec![ supported_bands: vec![
Band { Band {
low_hz: 1_800_000, low_hz: 1_800_000,
@@ -36,6 +36,7 @@ impl Ft450d {
model: "FT-450D".to_string(), model: "FT-450D".to_string(),
revision: "".to_string(), revision: "".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 10,
supported_bands: vec![ supported_bands: vec![
// Transmit-capable amateur bands (HF + 6m) // Transmit-capable amateur bands (HF + 6m)
Band { Band {
@@ -37,6 +37,7 @@ impl Ft817 {
model: "FT-817".to_string(), model: "FT-817".to_string(),
revision: "".to_string(), revision: "".to_string(),
capabilities: RigCapabilities { capabilities: RigCapabilities {
min_freq_step_hz: 10,
supported_bands: vec![ supported_bands: vec![
// Transmit-capable amateur bands // Transmit-capable amateur bands
Band { Band {