From bf74044c054b82cb04926b83a3947270ed893da2 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Thu, 5 Mar 2026 19:48:20 +0100 Subject: [PATCH] [fix](trx): normalize FT8 history freq and add map locator links --- .../trx-frontend-http/assets/web/app.js | 54 ++++++++++++++++++ .../assets/web/plugins/ft8.js | 35 ++++++++++-- .../assets/web/plugins/wspr.js | 56 ++++++++++++++++++- .../trx-frontend-http/assets/web/style.css | 3 + src/trx-server/src/audio.rs | 8 ++- src/trx-server/src/pskreporter.rs | 13 ++++- 6 files changed, 160 insertions(+), 9 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 2b8437a..cf0431f 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 @@ -4417,6 +4417,60 @@ window.navigateToAprsMap = function(lat, lon) { } }; +window.navigateToMapLocator = function(grid, preferredType = null) { + const normalizedGrid = String(grid || "").trim().toUpperCase(); + if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false; + + document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active")); + const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']"); + if (mapTabBtn) mapTabBtn.classList.add("active"); + document.querySelectorAll(".tab-panel").forEach((p) => (p.style.display = "none")); + const mapPanel = document.getElementById("tab-map"); + if (mapPanel) mapPanel.style.display = ""; + + initAprsMap(); + sizeAprsMapToViewport(); + if (!aprsMap) return false; + + const pref = preferredType === "wspr" ? "wspr" : (preferredType === "ft8" ? "ft8" : null); + const keys = pref + ? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`] + : [`ft8:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`]; + let entry = null; + for (const key of keys) { + entry = locatorMarkers.get(key); + if (entry?.marker) break; + } + if (!entry?.marker) return false; + + if (pref && Object.prototype.hasOwnProperty.call(mapFilter, pref) && !mapFilter[pref]) { + mapFilter[pref] = true; + rebuildMapLocatorFilters(); + applyMapFilter(); + } + + const marker = entry.marker; + if (!aprsMap.hasLayer(marker)) { + marker.addTo(aprsMap); + sendLocatorOverlayToBack(marker); + } + const center = locatorMarkerCenter(marker); + const focusMarker = () => { + if (!aprsMap || !marker) return; + aprsMap.invalidateSize(); + if (center) { + const targetZoom = Math.max(aprsMap.getZoom() || 0, 7); + aprsMap.setView([center.lat, center.lon], targetZoom); + setMapRadioPathTo(center.lat, center.lon, "locator-radio-path"); + } + setSelectedLocatorMarker(marker); + if (typeof marker.openPopup === "function") marker.openPopup(); + }; + focusMarker(); + setTimeout(focusMarker, 60); + return true; +}; + function haversineKm(lat1, lon1, lat2, lon2) { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js index b146530..1697774 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js @@ -43,6 +43,7 @@ function renderFt8Row(msg) { row.className = "ft8-row"; const rawMessage = (msg.message || "").toString(); row.dataset.message = rawMessage.toUpperCase(); + row.dataset.decoder = "ft8"; row.dataset.storedFreqHz = Number.isFinite(msg.freq_hz) ? String(msg.freq_hz) : ""; const snr = Number.isFinite(msg.snr_db) ? msg.snr_db.toFixed(1) : "--"; const dt = Number.isFinite(msg.dt_s) ? msg.dt_s.toFixed(2) : "--"; @@ -131,7 +132,7 @@ function renderFt8Message(message) { const token = message.slice(i, j); const grid = token.toUpperCase(); if (isMaidenheadGridToken(grid)) { - out += `${grid}`; + out += `${grid}`; } else { out += escapeHtml(token); } @@ -200,6 +201,17 @@ function isAlphaNum(ch) { return /[A-Za-z0-9]/.test(ch); } +function activateFt8HistoryLocator(targetEl) { + const locatorEl = targetEl?.closest?.(".ft8-locator[data-locator-grid]"); + if (!locatorEl) return false; + const grid = String(locatorEl.dataset.locatorGrid || "").toUpperCase(); + if (!grid) return false; + if (typeof window.navigateToMapLocator === "function") { + window.navigateToMapLocator(grid, "ft8"); + } + return true; +} + function applyFt8FilterToRow(row) { if (!ft8FilterText) { row.style.display = ""; @@ -248,6 +260,20 @@ if (ft8FilterInput) { }); } +if (ft8MessagesEl) { + ft8MessagesEl.addEventListener("click", (event) => { + if (!activateFt8HistoryLocator(event.target)) return; + event.preventDefault(); + event.stopPropagation(); + }); + ft8MessagesEl.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + if (!activateFt8HistoryLocator(event.target)) return; + event.preventDefault(); + event.stopPropagation(); + }); +} + if (ft8PauseBtn) { ft8PauseBtn.addEventListener("click", () => { ft8Paused = !ft8Paused; @@ -279,10 +305,7 @@ window.onServerFt8 = function(msg) { const raw = (msg.message || "").toString(); const grids = extractAllGrids(raw); const station = extractLikelyCallsign(raw); - const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null; - const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) - ? (baseHz + Number(msg.freq_hz)) - : (Number.isFinite(msg.freq_hz) ? Number(msg.freq_hz) : null); + const rfHz = normalizeFt8DisplayFreqHz(msg.freq_hz); if (grids.length > 0 && window.ft8MapAddLocator) { window.ft8MapAddLocator(raw, grids, "ft8", station, { ...msg, @@ -294,7 +317,7 @@ window.onServerFt8 = function(msg) { ts_ms: msg.ts_ms, snr_db: msg.snr_db, dt_s: msg.dt_s, - freq_hz: msg.freq_hz, + freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz, message: msg.message, }); }; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js index ddd3c19..4063101 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js @@ -31,6 +31,7 @@ setInterval(updateWsprPeriodTimer, 500); function renderWsprRow(msg) { const row = document.createElement("div"); row.className = "ft8-row"; + row.dataset.decoder = "wspr"; const snr = Number.isFinite(msg.snr_db) ? msg.snr_db.toFixed(1) : "--"; const dt = Number.isFinite(msg.dt_s) ? msg.dt_s.toFixed(2) : "--"; const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null; @@ -38,7 +39,7 @@ function renderWsprRow(msg) { const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--"; const message = (msg.message || "").toString(); row.dataset.message = message.toUpperCase(); - row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${snr}${dt}${freq}${escapeWsprHtml(message)}`; + row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${snr}${dt}${freq}${renderWsprMessage(message)}`; applyWsprFilterToRow(row); return row; } @@ -80,6 +81,30 @@ function escapeWsprHtml(input) { .replaceAll("\"", """); } +function renderWsprMessage(message) { + let out = ""; + let i = 0; + while (i < message.length) { + const ch = message[i]; + if (isAlphaNum(ch)) { + let j = i + 1; + while (j < message.length && isAlphaNum(message[j])) j++; + const token = message.slice(i, j); + const grid = token.toUpperCase(); + if (isMaidenheadGridToken(grid)) { + out += `${grid}`; + } else { + out += escapeWsprHtml(token); + } + i = j; + } else { + out += escapeWsprHtml(ch); + i += 1; + } + } + return out; +} + function extractAllGrids(message) { const out = []; const seen = new Set(); @@ -116,6 +141,21 @@ function isMaidenheadGridToken(token) { return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized); } +function isAlphaNum(ch) { + return /[A-Za-z0-9]/.test(ch); +} + +function activateWsprHistoryLocator(targetEl) { + const locatorEl = targetEl?.closest?.(".ft8-locator[data-locator-grid]"); + if (!locatorEl) return false; + const grid = String(locatorEl.dataset.locatorGrid || "").toUpperCase(); + if (!grid) return false; + if (typeof window.navigateToMapLocator === "function") { + window.navigateToMapLocator(grid, "wspr"); + } + return true; +} + function applyWsprFilterToRow(row) { if (!wsprFilterText) { row.style.display = ""; @@ -145,6 +185,20 @@ if (wsprFilterInput) { }); } +if (wsprMessagesEl) { + wsprMessagesEl.addEventListener("click", (event) => { + if (!activateWsprHistoryLocator(event.target)) return; + event.preventDefault(); + event.stopPropagation(); + }); + wsprMessagesEl.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + if (!activateWsprHistoryLocator(event.target)) return; + event.preventDefault(); + event.stopPropagation(); + }); +} + if (wsprPauseBtn) { wsprPauseBtn.addEventListener("click", () => { wsprPaused = !wsprPaused; 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 9e04406..f526fb8 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 @@ -1634,6 +1634,9 @@ small { color: var(--text-muted); } .ft8-freq { color: var(--accent-green); min-width: 4.6rem; text-align: right; } .ft8-msg { flex: 1; } .ft8-locator { color: var(--accent-green); background: rgba(0, 209, 127, 0.12); border: 1px solid rgba(0, 209, 127, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-weight: 600; } +.ft8-locator[data-locator-grid] { cursor: pointer; user-select: none; } +.ft8-locator[data-locator-grid]:hover { filter: brightness(1.12); } +.ft8-locator[data-locator-grid]:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent-green) 66%, transparent); outline-offset: 1px; } .map-locator-filters { display: flex; flex-direction: column; diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 980bb22..7e807e8 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -1341,11 +1341,17 @@ pub async fn run_ft8_decoder( Ok(dur) => dur.as_millis() as i64, Err(_) => 0, }; + let base_freq_hz = state_rx.borrow().status.freq.hz as f64; + let abs_freq_hz = base_freq_hz + res.freq_hz as f64; let msg = Ft8Message { ts_ms, snr_db: res.snr_db, dt_s: res.dt_s, - freq_hz: res.freq_hz, + freq_hz: if abs_freq_hz.is_finite() && abs_freq_hz > 0.0 { + abs_freq_hz as f32 + } else { + res.freq_hz + }, message: res.text, }; histories.record_ft8_message(msg.clone()); diff --git a/src/trx-server/src/pskreporter.rs b/src/trx-server/src/pskreporter.rs index d6c1dff..b5c88a3 100644 --- a/src/trx-server/src/pskreporter.rs +++ b/src/trx-server/src/pskreporter.rs @@ -179,7 +179,12 @@ fn now_unix_seconds() -> u32 { } fn offset_to_abs(base_freq_hz: u64, offset_hz: f32) -> u64 { - let freq = base_freq_hz as f64 + offset_hz as f64; + // Accept both legacy decoder offsets (~kHz audio tones) and already-absolute RF Hz. + let raw = offset_hz as f64; + if raw.is_finite() && raw >= 100_000.0 { + return raw.round() as u64; + } + let freq = base_freq_hz as f64 + raw; if freq.is_finite() && freq > 0.0 { freq.round() as u64 } else { @@ -454,4 +459,10 @@ mod tests { let grid = maidenhead_from_lat_lon(52.2297, 21.0122); assert_eq!(grid.len(), 6); } + + #[test] + fn offset_to_abs_accepts_offset_and_absolute() { + assert_eq!(offset_to_abs(14_074_000, 1_237.0), 14_075_237); + assert_eq!(offset_to_abs(14_074_000, 14_075_237.0), 14_075_237); + } }