[feat](trx-frontend): improve spectrum tuning controls
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1158,6 +1158,103 @@ function applyLocalTunedFrequency(hz, forceDisplay = false) {
|
||||
positionRdsPsOverlay();
|
||||
}
|
||||
|
||||
function coverageGuardBandwidthHz(mode = modeEl ? modeEl.value : "") {
|
||||
const [, , maxBw] = mwDefaultsForMode(mode);
|
||||
return Math.max(0, Number.isFinite(maxBw) ? maxBw : currentBandwidthHz);
|
||||
}
|
||||
|
||||
function requiredCenterFreqForCoverage(freqHz, bandwidthHz = coverageGuardBandwidthHz()) {
|
||||
if (!lastSpectrumData || !Number.isFinite(freqHz)) return null;
|
||||
const sampleRate = Number(lastSpectrumData.sample_rate);
|
||||
const currentCenterHz = Number(lastSpectrumData.center_hz);
|
||||
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || !Number.isFinite(currentCenterHz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const safeBw = Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0);
|
||||
const halfSpanHz = sampleRate / 2;
|
||||
const requiredHalfSpanHz = safeBw / 2 + SPECTRUM_COVERAGE_MARGIN_HZ;
|
||||
if (requiredHalfSpanHz * 2 >= sampleRate) {
|
||||
return alignFreqToRigStep(Math.round(freqHz));
|
||||
}
|
||||
|
||||
const currentLoHz = currentCenterHz - halfSpanHz;
|
||||
const currentHiHz = currentCenterHz + halfSpanHz;
|
||||
const requiredLoHz = freqHz - requiredHalfSpanHz;
|
||||
const requiredHiHz = freqHz + requiredHalfSpanHz;
|
||||
if (requiredLoHz >= currentLoHz && requiredHiHz <= currentHiHz) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextCenterHz = currentCenterHz;
|
||||
if (requiredLoHz < currentLoHz) {
|
||||
nextCenterHz = requiredLoHz + halfSpanHz;
|
||||
}
|
||||
if (requiredHiHz > currentHiHz) {
|
||||
nextCenterHz = requiredHiHz - halfSpanHz;
|
||||
}
|
||||
return alignFreqToRigStep(Math.round(nextCenterHz));
|
||||
}
|
||||
|
||||
async function ensureTunedBandwidthCoverage(freqHz, bandwidthHz = coverageGuardBandwidthHz()) {
|
||||
const nextCenterHz = requiredCenterFreqForCoverage(freqHz, bandwidthHz);
|
||||
if (!Number.isFinite(nextCenterHz)) return;
|
||||
if (lastSpectrumData && Math.abs(nextCenterHz - Number(lastSpectrumData.center_hz)) < 1) return;
|
||||
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
|
||||
if (centerFreqEl && !centerFreqDirty) {
|
||||
centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit);
|
||||
}
|
||||
}
|
||||
|
||||
async function setRigFrequency(freqHz) {
|
||||
const targetHz = Math.round(freqHz);
|
||||
await postPath(`/set_freq?hz=${targetHz}`);
|
||||
applyLocalTunedFrequency(targetHz);
|
||||
await ensureTunedBandwidthCoverage(targetHz);
|
||||
}
|
||||
|
||||
function tunedFrequencyForCenterCoverage(centerHz, freqHz = lastFreqHz, bandwidthHz = coverageGuardBandwidthHz()) {
|
||||
if (!Number.isFinite(centerHz) || !Number.isFinite(freqHz) || !lastSpectrumData) return null;
|
||||
const sampleRate = Number(lastSpectrumData.sample_rate);
|
||||
if (!Number.isFinite(sampleRate) || sampleRate <= 0) return null;
|
||||
|
||||
const safeBw = Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0);
|
||||
const halfSpanHz = sampleRate / 2;
|
||||
const requiredHalfSpanHz = safeBw / 2 + SPECTRUM_COVERAGE_MARGIN_HZ;
|
||||
if (requiredHalfSpanHz * 2 >= sampleRate) {
|
||||
return alignFreqToRigStep(Math.round(centerHz));
|
||||
}
|
||||
|
||||
const minFreqHz = centerHz - halfSpanHz + requiredHalfSpanHz;
|
||||
const maxFreqHz = centerHz + halfSpanHz - requiredHalfSpanHz;
|
||||
if (freqHz >= minFreqHz && freqHz <= maxFreqHz) {
|
||||
return null;
|
||||
}
|
||||
const clampedHz = Math.max(minFreqHz, Math.min(maxFreqHz, freqHz));
|
||||
return alignFreqToRigStep(Math.round(clampedHz));
|
||||
}
|
||||
|
||||
async function shiftSpectrumCenter(direction) {
|
||||
if (!lastSpectrumData || !Number.isFinite(direction) || direction === 0) return;
|
||||
const sampleRate = Number(lastSpectrumData.sample_rate);
|
||||
const currentCenterHz = Number(lastSpectrumData.center_hz);
|
||||
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || !Number.isFinite(currentCenterHz)) return;
|
||||
|
||||
const stepHz = Math.max(50_000, Math.round(sampleRate * 0.35));
|
||||
const nextCenterHz = alignFreqToRigStep(Math.round(currentCenterHz + direction * stepHz));
|
||||
showHint("Shifting spectrum…", 900);
|
||||
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
|
||||
if (centerFreqEl && !centerFreqDirty) {
|
||||
centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit);
|
||||
}
|
||||
|
||||
const nextFreqHz = tunedFrequencyForCenterCoverage(nextCenterHz);
|
||||
if (Number.isFinite(nextFreqHz) && Math.abs(nextFreqHz - Number(lastFreqHz)) >= 1) {
|
||||
await postPath(`/set_freq?hz=${nextFreqHz}`);
|
||||
applyLocalTunedFrequency(nextFreqHz);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshCenterFreqDisplay() {
|
||||
if (!centerFreqEl || !lastSpectrumData || centerFreqDirty) return;
|
||||
centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit);
|
||||
@@ -1168,7 +1265,8 @@ function parseFreqInput(val, defaultStep) {
|
||||
const trimmed = val.trim().toLowerCase();
|
||||
const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/);
|
||||
if (!match) return null;
|
||||
let num = parseFloat(match[1].replace(",", "."));
|
||||
const rawNumber = match[1];
|
||||
let num = parseFloat(rawNumber.replace(",", "."));
|
||||
const unit = match[2] || "";
|
||||
if (Number.isNaN(num)) return null;
|
||||
if (unit.startsWith("gh") || unit === "g") {
|
||||
@@ -1178,6 +1276,18 @@ function parseFreqInput(val, defaultStep) {
|
||||
} else if (unit.startsWith("kh") || unit === "k") {
|
||||
num *= 1_000;
|
||||
} else if (!unit) {
|
||||
const mode = (modeEl?.value || "").toUpperCase();
|
||||
const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
|
||||
if (mode === "WFM") {
|
||||
if (hasDecimalSeparator && num >= 50 && num < 200) {
|
||||
num *= 1_000_000;
|
||||
return Math.round(num);
|
||||
}
|
||||
if (!hasDecimalSeparator && num >= 875 && num <= 1080) {
|
||||
num = (num / 10) * 1_000_000;
|
||||
return Math.round(num);
|
||||
}
|
||||
}
|
||||
// Use currently selected input unit when user omits suffix.
|
||||
if (defaultStep >= 1_000_000) {
|
||||
num *= 1_000_000;
|
||||
@@ -1314,6 +1424,7 @@ let serverActiveRigId = null;
|
||||
let serverLat = null;
|
||||
let serverLon = null;
|
||||
let initialMapZoom = 10;
|
||||
const SPECTRUM_COVERAGE_MARGIN_HZ = 50_000;
|
||||
|
||||
function updateFooterBuildInfo() {
|
||||
const serverEl = document.getElementById("footer-server-build");
|
||||
@@ -1909,8 +2020,7 @@ async function applyFreqFromInput() {
|
||||
freqEl.disabled = true;
|
||||
showHint("Setting frequency…");
|
||||
try {
|
||||
await postPath(`/set_freq?hz=${parsed}`);
|
||||
applyLocalTunedFrequency(parsed);
|
||||
await setRigFrequency(parsed);
|
||||
showHint("Freq set", 1500);
|
||||
} catch (err) {
|
||||
showHint("Set freq failed", 2000);
|
||||
@@ -1961,6 +2071,11 @@ if (centerFreqEl) {
|
||||
applyCenterFreqFromInput();
|
||||
}
|
||||
});
|
||||
centerFreqEl.addEventListener("wheel", (e) => {
|
||||
e.preventDefault();
|
||||
const direction = e.deltaY < 0 ? 1 : -1;
|
||||
jogFreq(direction);
|
||||
}, { passive: false });
|
||||
}
|
||||
freqEl.addEventListener("wheel", (e) => {
|
||||
e.preventDefault();
|
||||
@@ -2009,8 +2124,7 @@ async function jogFreq(direction) {
|
||||
jogIndicator.style.transform = `translateX(-50%) rotate(${jogAngle}deg)`;
|
||||
showHint("Setting frequency…");
|
||||
try {
|
||||
await postPath(`/set_freq?hz=${newHz}`);
|
||||
applyLocalTunedFrequency(newHz);
|
||||
await setRigFrequency(newHz);
|
||||
showHint("Freq set", 1000);
|
||||
} catch (err) {
|
||||
showHint("Set freq failed", 2000);
|
||||
@@ -2256,7 +2370,12 @@ async function applyBandwidthFromInput() {
|
||||
currentBandwidthHz = clamped;
|
||||
syncBandwidthInput(clamped);
|
||||
if (lastSpectrumData) scheduleSpectrumDraw();
|
||||
try { await postPath(`/set_bandwidth?hz=${clamped}`); } catch (_) {}
|
||||
try {
|
||||
await postPath(`/set_bandwidth?hz=${clamped}`);
|
||||
if (Number.isFinite(lastFreqHz)) {
|
||||
await ensureTunedBandwidthCoverage(lastFreqHz);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function estimateBandwidthAroundPeak(data, centerHz) {
|
||||
@@ -2326,6 +2445,9 @@ async function applyAutoBandwidth() {
|
||||
if (lastSpectrumData) scheduleSpectrumDraw();
|
||||
try {
|
||||
await postPath(`/set_bandwidth?hz=${estimated}`);
|
||||
if (Number.isFinite(lastFreqHz)) {
|
||||
await ensureTunedBandwidthCoverage(lastFreqHz);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -3539,6 +3661,8 @@ window.addEventListener("beforeunload", () => {
|
||||
const spectrumCanvas = document.getElementById("spectrum-canvas");
|
||||
const spectrumFreqAxis = document.getElementById("spectrum-freq-axis");
|
||||
const spectrumTooltip = document.getElementById("spectrum-tooltip");
|
||||
const spectrumCenterLeftBtn = document.getElementById("spectrum-center-left-btn");
|
||||
const spectrumCenterRightBtn = document.getElementById("spectrum-center-right-btn");
|
||||
let spectrumSource = null;
|
||||
let spectrumReconnectTimer = null;
|
||||
let spectrumDrawPending = false;
|
||||
@@ -3908,8 +4032,7 @@ async function tuneRdsAlternativeFrequency(hz) {
|
||||
if (!Number.isFinite(hz) || hz <= 0) return;
|
||||
const targetHz = Math.round(hz);
|
||||
try {
|
||||
await postPath(`/set_freq?hz=${targetHz}`);
|
||||
applyLocalTunedFrequency(targetHz);
|
||||
await setRigFrequency(targetHz);
|
||||
showHint(`Tuned ${formatRdsAfMHz(targetHz)}`, 1200);
|
||||
} catch (_) {
|
||||
showHint("Set freq failed", 1500);
|
||||
@@ -4673,8 +4796,7 @@ if (spectrumCanvas) {
|
||||
const cssX = e.clientX - rect.left;
|
||||
const targetHz = spectrumTargetHzAt(cssX, rect.width, lastSpectrumData);
|
||||
if (!Number.isFinite(targetHz)) return;
|
||||
postPath(`/set_freq?hz=${targetHz}`)
|
||||
.then(() => { applyLocalTunedFrequency(targetHz); })
|
||||
setRigFrequency(targetHz)
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
@@ -4716,12 +4838,22 @@ if (overviewCanvas) {
|
||||
const cssX = e.clientX - rect.left;
|
||||
const targetHz = spectrumTargetHzAt(cssX, rect.width, lastSpectrumData);
|
||||
if (!Number.isFinite(targetHz)) return;
|
||||
postPath(`/set_freq?hz=${targetHz}`)
|
||||
.then(() => { applyLocalTunedFrequency(targetHz); })
|
||||
setRigFrequency(targetHz)
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
if (spectrumCenterLeftBtn) {
|
||||
spectrumCenterLeftBtn.addEventListener("click", () => {
|
||||
shiftSpectrumCenter(-1).catch(() => {});
|
||||
});
|
||||
}
|
||||
if (spectrumCenterRightBtn) {
|
||||
spectrumCenterRightBtn.addEventListener("click", () => {
|
||||
shiftSpectrumCenter(1).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Spectrum floor input + Auto level ────────────────────────────────────────
|
||||
(function () {
|
||||
const floorInput = document.getElementById("spectrum-floor-input");
|
||||
|
||||
@@ -81,7 +81,9 @@
|
||||
<div id="spectrum-bookmark-axis"></div>
|
||||
<div id="spectrum-panel" style="display:none;">
|
||||
<div class="spectrum-wrap">
|
||||
<button id="spectrum-center-left-btn" class="spectrum-edge-shift spectrum-edge-shift-left" type="button" aria-label="Shift spectrum center left">‹</button>
|
||||
<canvas id="spectrum-canvas"></canvas>
|
||||
<button id="spectrum-center-right-btn" class="spectrum-edge-shift spectrum-edge-shift-right" type="button" aria-label="Shift spectrum center right">›</button>
|
||||
<div id="spectrum-tooltip"></div>
|
||||
<div id="spectrum-freq-axis"></div>
|
||||
</div>
|
||||
|
||||
@@ -1390,6 +1390,17 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
gap: 0.5rem;
|
||||
padding-top: 0.45rem;
|
||||
}
|
||||
.spectrum-edge-shift {
|
||||
width: 1.5rem;
|
||||
height: 2.35rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.spectrum-edge-shift-left {
|
||||
left: 0.28rem;
|
||||
}
|
||||
.spectrum-edge-shift-right {
|
||||
right: 0.28rem;
|
||||
}
|
||||
#spectrum-bw-row,
|
||||
#spectrum-level-row {
|
||||
gap: 0.4rem;
|
||||
@@ -1498,6 +1509,41 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
}
|
||||
.spectrum-edge-shift {
|
||||
position: absolute;
|
||||
top: calc(var(--spectrum-plot-height) / 2);
|
||||
transform: translateY(-50%);
|
||||
z-index: 8;
|
||||
width: 1.7rem;
|
||||
height: 2.8rem;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 85%, transparent);
|
||||
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
|
||||
color: var(--accent-yellow);
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.18);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.spectrum-edge-shift:hover {
|
||||
background: color-mix(in srgb, var(--btn-bg) 84%, transparent);
|
||||
color: var(--text-heading);
|
||||
border-color: var(--accent-yellow);
|
||||
}
|
||||
.spectrum-edge-shift:active {
|
||||
transform: translateY(-50%) scale(0.97);
|
||||
}
|
||||
.spectrum-edge-shift-left {
|
||||
left: 0.45rem;
|
||||
}
|
||||
.spectrum-edge-shift-right {
|
||||
right: 0.45rem;
|
||||
}
|
||||
#spectrum-freq-axis {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
|
||||
Reference in New Issue
Block a user