From eac5e142dbb165da65ce197e3d0813e192817e7f Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Wed, 4 Mar 2026 22:31:08 +0100 Subject: [PATCH] [fix](trx-frontend): rework cw auto and tone picker behavior Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 16 +- .../trx-frontend-http/assets/web/index.html | 4 +- .../assets/web/plugins/cw.js | 138 ++++++++++++++---- .../trx-frontend-http/assets/web/style.css | 3 + 4 files changed, 130 insertions(+), 31 deletions(-) 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 db73129..1148691 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 @@ -2302,10 +2302,20 @@ function render(update) { if (cwToneEl && typeof update.cw_tone_hz === "number") { cwToneEl.value = update.cw_tone_hz; } - if (cwWpmEl && typeof update.cw_auto === "boolean") { + if ((cwWpmEl || cwToneEl) && typeof update.cw_auto === "boolean") { const disabled = update.cw_auto; - cwWpmEl.disabled = disabled; - cwWpmEl.readOnly = disabled; + if (typeof window.applyCwAutoUi === "function") { + window.applyCwAutoUi(disabled); + } else { + if (cwWpmEl) { + cwWpmEl.disabled = disabled; + cwWpmEl.readOnly = disabled; + } + if (cwToneEl) { + cwToneEl.disabled = disabled; + cwToneEl.readOnly = disabled; + } + } } let activeFreqColor = "var(--accent-green)"; if (update.status && update.status.vfo && Array.isArray(update.status.vfo.entries)) { 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 787a853..439734d 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 @@ -554,13 +554,13 @@
- +
- CW Tone From Selected BW + CW Tone Picker --
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js index c599ef7..47bac8b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js @@ -6,10 +6,15 @@ const cwWpmInput = document.getElementById("cw-wpm"); const cwToneInput = document.getElementById("cw-tone"); const cwSignalIndicator = document.getElementById("cw-signal-indicator"); const cwToneCanvas = document.getElementById("cw-tone-waterfall"); +const cwTonePickerEl = document.querySelector(".cw-tone-picker"); const cwToneRangeEl = document.getElementById("cw-tone-range"); const CW_MAX_LINES = 200; const CW_TONE_MIN_HZ = 300; const CW_TONE_MAX_HZ = 1200; +const CW_WPM_MIN = 5; +const CW_WPM_MAX = 40; +let cwLastAppendTime = 0; +let cwTonePickerRaf = null; function applyCwAutoUi(enabled) { if (cwAutoInput) cwAutoInput.checked = enabled; @@ -17,6 +22,20 @@ function applyCwAutoUi(enabled) { cwWpmInput.disabled = enabled; cwWpmInput.readOnly = enabled; } + if (cwToneInput) { + cwToneInput.disabled = enabled; + cwToneInput.readOnly = enabled; + } + if (cwTonePickerEl) { + cwTonePickerEl.classList.toggle("is-auto", enabled); + } +} +window.applyCwAutoUi = applyCwAutoUi; + +function clampCwWpm(wpm) { + const numeric = Number(wpm); + if (!Number.isFinite(numeric)) return 15; + return Math.round(Math.max(CW_WPM_MIN, Math.min(CW_WPM_MAX, numeric))); } function clampCwTone(tone) { @@ -26,31 +45,56 @@ function clampCwTone(tone) { } function currentCwToneRange() { - const centerHz = Number.isFinite(window.lastFreqHz) ? Number(window.lastFreqHz) : Number.NaN; - const bandwidthHz = Number.isFinite(window.currentBandwidthHz) ? Number(window.currentBandwidthHz) : Number.NaN; + const centerHz = Number.isFinite(window.lastFreqHz) ? Number(window.lastFreqHz) : NaN; + const bandwidthHz = Number.isFinite(window.currentBandwidthHz) ? Number(window.currentBandwidthHz) : NaN; if (!Number.isFinite(centerHz) || !Number.isFinite(bandwidthHz) || bandwidthHz <= 0) { return null; } const mode = String(document.getElementById("mode")?.value || "").toUpperCase(); - const lowerSideband = mode === "CWR" || mode === "LSB"; - const halfBwHz = bandwidthHz / 2; + const lowerSideband = mode === "CWR"; + const upperSideband = mode === "CW"; + if (!lowerSideband && !upperSideband) return null; + const lowHz = lowerSideband ? centerHz - bandwidthHz : centerHz; + const highHz = lowerSideband ? centerHz : centerHz + bandwidthHz; const toneMinHz = CW_TONE_MIN_HZ; - const toneMaxHz = Math.max(toneMinHz, Math.min(CW_TONE_MAX_HZ, Math.round(halfBwHz))); + const toneMaxHz = Math.min(CW_TONE_MAX_HZ, Math.round(bandwidthHz)); if (toneMaxHz < toneMinHz) { return null; } return { - lowHz: centerHz - halfBwHz, - highHz: centerHz + halfBwHz, + lowHz, + highHz, centerHz, bandwidthHz, - halfBwHz, toneMinHz, toneMaxHz, lowerSideband, + mode, }; } +function toneClampForRange(tone, range) { + const clamped = clampCwTone(tone); + if (!range) return clamped; + return Math.max(range.toneMinHz, Math.min(range.toneMaxHz, clamped)); +} + +function ensureCwToneCanvasResolution() { + if (!cwToneCanvas) return false; + const rect = cwToneCanvas.getBoundingClientRect(); + const cssWidth = Math.max(1, Math.round(rect.width)); + const cssHeight = Math.max(1, Math.round(rect.height)); + const dpr = window.devicePixelRatio || 1; + const nextWidth = Math.max(1, Math.round(cssWidth * dpr)); + const nextHeight = Math.max(1, Math.round(cssHeight * dpr)); + if (cwToneCanvas.width !== nextWidth || cwToneCanvas.height !== nextHeight) { + cwToneCanvas.width = nextWidth; + cwToneCanvas.height = nextHeight; + return true; + } + return false; +} + function drawCwTonePicker() { if (!cwToneCanvas) return; const ctx = cwToneCanvas.getContext("2d"); @@ -62,14 +106,22 @@ function drawCwTonePicker() { const range = currentCwToneRange(); if (!range || !window.lastSpectrumData || !Array.isArray(window.lastSpectrumData.bins) || !window.lastSpectrumData.bins.length) { - if (cwToneRangeEl) cwToneRangeEl.textContent = "Waiting for spectrum"; + if (cwToneRangeEl) { + const mode = String(document.getElementById("mode")?.value || "").toUpperCase(); + if (mode !== "CW" && mode !== "CWR") { + cwToneRangeEl.textContent = "CW/CWR mode required"; + } else { + cwToneRangeEl.textContent = "Waiting for spectrum"; + } + } ctx.fillStyle = "rgba(130, 150, 165, 0.22)"; ctx.fillRect(0, 0, width, height); return; } if (cwToneRangeEl) { - cwToneRangeEl.textContent = `${(range.bandwidthHz / 1000).toFixed(range.bandwidthHz >= 10_000 ? 0 : 1)} kHz span`; + const side = range.lowerSideband ? "Lower side" : "Upper side"; + cwToneRangeEl.textContent = `${side} ยท Tone ${range.toneMinHz}-${range.toneMaxHz} Hz`; } const bins = window.lastSpectrumData.bins; @@ -99,7 +151,7 @@ function drawCwTonePicker() { ctx.fillRect(x, 0, 1, height); } - const currentTone = clampCwTone(cwToneInput ? cwToneInput.value : 700); + const currentTone = toneClampForRange(cwToneInput ? cwToneInput.value : 700, range); const markerHz = range.lowerSideband ? range.centerHz - currentTone : range.centerHz + currentTone; @@ -108,14 +160,27 @@ function drawCwTonePicker() { ctx.fillStyle = "rgba(255, 255, 255, 0.9)"; ctx.fillRect(markerX, 0, 2, height); - const centerFrac = (range.centerHz - range.lowHz) / Math.max(1, (range.highHz - range.lowHz)); - const centerX = Math.max(0, Math.min(width - 1, Math.round(centerFrac * (width - 1)))); + const lowLimitHz = range.lowerSideband + ? range.centerHz - range.toneMaxHz + : range.centerHz + range.toneMinHz; + const highLimitHz = range.lowerSideband + ? range.centerHz - range.toneMinHz + : range.centerHz + range.toneMaxHz; + const limitLowX = Math.max(0, Math.min(width - 1, Math.round(((lowLimitHz - range.lowHz) / Math.max(1, range.highHz - range.lowHz)) * (width - 1)))); + const limitHighX = Math.max(0, Math.min(width - 1, Math.round(((highLimitHz - range.lowHz) / Math.max(1, range.highHz - range.lowHz)) * (width - 1)))); ctx.fillStyle = "rgba(255, 255, 255, 0.22)"; - ctx.fillRect(centerX, 0, 1, height); + ctx.fillRect(limitLowX, 0, 1, height); + ctx.fillRect(limitHighX, 0, 1, height); + + if (cwAutoInput?.checked) { + ctx.fillStyle = "rgba(0, 0, 0, 0.22)"; + ctx.fillRect(0, 0, width, height); + } } async function setCwTone(tone, { syncInput = true } = {}) { - const clamped = clampCwTone(tone); + const range = currentCwToneRange(); + const clamped = toneClampForRange(tone, range); if (cwToneInput && syncInput) { cwToneInput.value = clamped; } @@ -131,15 +196,19 @@ if (cwAutoInput) { cwAutoInput.addEventListener("change", async () => { const enabled = cwAutoInput.checked; applyCwAutoUi(enabled); - try { await postPath(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`); } - catch (e) { console.error("CW auto toggle failed", e); } + try { + await postPath(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`); + drawCwTonePicker(); + } catch (e) { + console.error("CW auto toggle failed", e); + } }); } if (cwWpmInput) { cwWpmInput.addEventListener("change", async () => { if (cwAutoInput && cwAutoInput.checked) return; - const wpm = Math.max(5, Math.min(40, Number(cwWpmInput.value))); + const wpm = clampCwWpm(cwWpmInput.value); cwWpmInput.value = wpm; try { await postPath(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`); } catch (e) { console.error("CW WPM set failed", e); } @@ -148,12 +217,14 @@ if (cwWpmInput) { if (cwToneInput) { cwToneInput.addEventListener("change", async () => { + if (cwAutoInput?.checked) return; await setCwTone(cwToneInput.value); }); } if (cwToneCanvas) { cwToneCanvas.addEventListener("click", async (event) => { + if (cwAutoInput?.checked) return; const rect = cwToneCanvas.getBoundingClientRect(); if (rect.width <= 0) return; const range = currentCwToneRange(); @@ -169,7 +240,7 @@ if (cwToneCanvas) { } window.resetCwHistoryView = function() { - cwOutputEl.innerHTML = ""; + if (cwOutputEl) cwOutputEl.innerHTML = ""; cwLastAppendTime = 0; drawCwTonePicker(); }; @@ -184,10 +255,9 @@ document.getElementById("cw-clear-btn").addEventListener("click", async () => { }); // --- Server-side CW decode handler --- -let cwLastAppendTime = 0; window.onServerCw = function(evt) { - cwStatusEl.textContent = "Receiving"; - if (evt.text) { + if (cwStatusEl) cwStatusEl.textContent = "Receiving"; + if (evt.text && cwOutputEl) { // Append decoded text to output const now = Date.now(); if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 10000 || evt.text === "\n") { @@ -205,12 +275,28 @@ window.onServerCw = function(evt) { } cwOutputEl.scrollTop = cwOutputEl.scrollHeight; } - cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off"; - if (!cwAutoInput || cwAutoInput.checked) { - cwWpmInput.value = evt.wpm; + if (cwSignalIndicator) { + cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off"; } - drawCwTonePicker(); + if (!cwAutoInput || cwAutoInput.checked) { + if (cwWpmInput && Number.isFinite(Number(evt.wpm))) { + cwWpmInput.value = clampCwWpm(evt.wpm); + } + if (cwToneInput && Number.isFinite(Number(evt.tone_hz))) { + cwToneInput.value = toneClampForRange(evt.tone_hz, currentCwToneRange()); + } + } + if (cwTonePickerRaf != null) return; + cwTonePickerRaf = requestAnimationFrame(() => { + cwTonePickerRaf = null; + drawCwTonePicker(); + }); }; window.refreshCwTonePicker = drawCwTonePicker; +window.addEventListener("resize", () => { + if (ensureCwToneCanvasResolution()) drawCwTonePicker(); +}); +applyCwAutoUi(!!cwAutoInput?.checked); +ensureCwToneCanvasResolution(); drawCwTonePicker(); 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 a83a15b..57463ef 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 @@ -1739,8 +1739,10 @@ small { color: var(--text-muted); } .cw-config label { display: flex; flex-direction: column; gap: 0.2rem; color: var(--text-muted); font-size: 0.82rem; } .cw-config input[type="number"] { width: 5rem; padding: 0.3rem 0.4rem; font-size: 0.9rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); color: var(--text); } .cw-tone-picker { margin-bottom: 0.75rem; border: 1px solid var(--border-light); border-radius: 8px; background: var(--input-bg); padding: 0.5rem 0.6rem; } +.cw-tone-picker.is-auto { opacity: 0.82; } .cw-tone-picker-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.35rem; color: var(--text-muted); font-size: 0.78rem; } #cw-tone-waterfall { width: 100%; height: 56px; display: block; border-radius: 6px; background: linear-gradient(180deg, rgba(8, 14, 18, 0.92), rgba(18, 28, 36, 0.98)); cursor: crosshair; } +.cw-tone-picker.is-auto #cw-tone-waterfall { cursor: not-allowed; } #cw-output { max-height: 360px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85rem; padding: 0.4rem 0.5rem; min-height: 60px; white-space: pre-wrap; word-break: break-all; } .cw-line { line-height: 1.5; } .cw-signal-on { width: 10px; height: 10px; border-radius: 50%; background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green); flex-shrink: 0; } @@ -1748,6 +1750,7 @@ small { color: var(--text-muted); } .cw-config .cw-auto-label { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; color: var(--text-muted); cursor: pointer; flex-direction: row; } .cw-auto-label input[type="checkbox"] { margin: 0; cursor: pointer; } .cw-config input[type="number"][readonly] { opacity: 0.6; } +.cw-config input[type="number"]:disabled { opacity: 0.58; } button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--accent-green);