import { hostCore } from "./chunk-KL66PICH.js"; // src/plugins/wspr.ts var wsprWindow = window; var wsprStatus = document.getElementById("wspr-status"); var wsprPeriodEl = document.getElementById("wspr-period"); var wsprMessagesEl = document.getElementById("wspr-messages"); var wsprFilterInput = document.getElementById("wspr-filter"); var WSPR_PERIOD_SECONDS = 120; var wsprFilterText = ""; var wsprMessageHistory = []; function finiteNumber(value) { const number = typeof value === "number" ? value : Number(value); return Number.isFinite(number) ? number : null; } function currentWsprHistoryRetentionMs() { return typeof wsprWindow.getDecodeHistoryRetentionMs === "function" ? wsprWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; } function pruneWsprMessageHistory() { const cutoffMs = Date.now() - currentWsprHistoryRetentionMs(); wsprMessageHistory = wsprMessageHistory.filter((msg) => Number(msg._tsMs ?? msg.ts_ms) >= cutoffMs); } function scheduleWsprHistoryRender() { if (typeof wsprWindow.trxScheduleUiFrameJob === "function") { wsprWindow.trxScheduleUiFrameJob("wspr-history", () => { renderWsprHistory(); }); return; } renderWsprHistory(); } function fmtWsprTime(tsMs) { if (!tsMs) return "--:--:--"; return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function updateWsprPeriodTimer() { if (!wsprPeriodEl) return; const nowSec = Math.floor(Date.now() / 1e3); const remaining = WSPR_PERIOD_SECONDS - nowSec % WSPR_PERIOD_SECONDS; const mm = String(Math.floor(remaining / 60)).padStart(2, "0"); const ss = String(remaining % 60).padStart(2, "0"); wsprPeriodEl.textContent = `Next slot ${mm}:${ss}`; } updateWsprPeriodTimer(); setInterval(updateWsprPeriodTimer, 500); function renderWsprRow(msg) { const row = document.createElement("div"); row.className = "ft8-row"; row.dataset.decoder = "wspr"; const snr = finiteNumber(msg.snr_db); const delta = finiteNumber(msg.dt_s); const baseHz = finiteNumber(wsprWindow.ft8BaseHz); const offsetHz = finiteNumber(msg.freq_hz); const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : null; const freq = rfHz?.toFixed(0) ?? "--"; const message = msg.message ?? ""; row.dataset.message = message.toUpperCase(); row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${freq}${renderWsprMessage(message)}`; applyWsprFilterToRow(row); return row; } function renderWsprHistory() { pruneWsprMessageHistory(); if (!wsprMessagesEl) return; const fragment = document.createDocumentFragment(); for (let i = 0; i < wsprMessageHistory.length; i += 1) { const message = wsprMessageHistory[i]; if (message) fragment.appendChild(renderWsprRow(message)); } wsprMessagesEl.replaceChildren(fragment); } function addWsprMessage(msg) { msg._tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); wsprMessageHistory.unshift(msg); pruneWsprMessageHistory(); scheduleWsprHistoryRender(); } function normalizeServerWsprMessage(msg) { const raw = msg.message ?? ""; const grids = extractAllGrids(raw); const station = extractLikelyCallsign(raw); const baseHz = finiteNumber(wsprWindow.ft8BaseHz); const offsetHz = finiteNumber(msg.freq_hz); const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : offsetHz; return { raw, grids, station, rfHz, history: { receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null, ts_ms: msg.ts_ms, snr_db: msg.snr_db, dt_s: msg.dt_s, freq_hz: msg.freq_hz, message: raw } }; } function onServerWsprBatch(messages) { if (!Array.isArray(messages) || messages.length === 0) return; if (wsprStatus) wsprStatus.textContent = "Receiving"; const normalized = []; for (const msg of messages) { const next = normalizeServerWsprMessage(msg); if (next.grids.length > 0 && wsprWindow.mapAddLocator) { wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, { ...msg, ...next.rfHz === null ? {} : { freq_hz: next.rfHz } }); } next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now(); normalized.push(next.history); } normalized.reverse(); wsprMessageHistory = normalized.concat(wsprMessageHistory); pruneWsprMessageHistory(); scheduleWsprHistoryRender(); } function pruneWsprHistoryView() { pruneWsprMessageHistory(); renderWsprHistory(); } function escapeWsprHtml(input) { return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").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 = /* @__PURE__ */ new Set(); const parts = message.toUpperCase().split(/[^A-Z0-9]+/); for (const token of parts) { if (!token) continue; if (isMaidenheadGridToken(token) && !seen.has(token)) { seen.add(token); out.push(token); } } return out; } function extractLikelyCallsign(message) { const parts = message.toUpperCase().split(/[^A-Z0-9/]+/); for (const token of parts) { if (!token) continue; if (token.length < 3 || token.length > 12) continue; if (token === "CQ" || token === "DE" || token === "QRZ" || token === "DX") continue; if (isMaidenheadGridToken(token)) continue; if (/^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token)) return token; } return null; } function isFtxFarewellToken(token) { const normalized = token.trim().toUpperCase(); return normalized === "RR73" || normalized === "73" || normalized === "RR"; } function isMaidenheadGridToken(token) { const normalized = token.trim().toUpperCase(); return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized); } function isAlphaNum(ch) { return ch !== void 0 && /[A-Za-z0-9]/.test(ch); } function activateWsprHistoryLocator(target) { if (!(target instanceof Element)) return false; const locatorEl = target.closest(".ft8-locator[data-locator-grid]"); if (!locatorEl) return false; const grid = (locatorEl.dataset.locatorGrid || "").toUpperCase(); if (!grid) return false; if (typeof wsprWindow.navigateToMapLocator === "function") { wsprWindow.navigateToMapLocator(grid, "wspr"); } return true; } function applyWsprFilterToRow(row) { if (!wsprFilterText) { row.style.display = ""; return; } const message = row.dataset.message || ""; row.style.display = message.includes(wsprFilterText) ? "" : "none"; } function resetWsprHistoryView() { if (wsprMessagesEl) wsprMessagesEl.innerHTML = ""; wsprMessageHistory = []; renderWsprHistory(); if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr"); } if (wsprFilterInput) { wsprFilterInput.addEventListener("input", () => { wsprFilterText = wsprFilterInput.value.trim().toUpperCase(); renderWsprHistory(); }); } 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(); }); } var wsprDecodeToggleBtn = document.getElementById("wspr-decode-toggle-btn"); wsprDecodeToggleBtn?.addEventListener("click", () => { void (async () => { try { await wsprWindow.takeSchedulerControlForDecoderDisable?.(wsprDecodeToggleBtn); await hostCore.postPath("/toggle_wspr_decode"); } catch (error) { console.error("WSPR toggle failed", error); } })(); }); document.getElementById("settings-clear-wspr-history")?.addEventListener("click", () => { void (async () => { if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return; try { await hostCore.postPath("/clear_wspr_decode"); resetWsprHistoryView(); } catch (error) { console.error("WSPR history clear failed", error); } })(); }); function onServerWspr(msg) { if (wsprStatus) wsprStatus.textContent = "Receiving"; const next = normalizeServerWsprMessage(msg); if (next.grids.length > 0 && wsprWindow.mapAddLocator) { wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, { ...msg, ...next.rfHz === null ? {} : { freq_hz: next.rfHz } }); } addWsprMessage(next.history); } wsprWindow.trxPluginRuntime.registerDecoder({ id: "wspr", onMessage: onServerWspr, onBatch: onServerWsprBatch, restore: onServerWsprBatch, prune: pruneWsprHistoryView, reset: resetWsprHistoryView });