diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js index 583fcf06..bf10f860 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js @@ -6,8 +6,58 @@ const number = typeof value === "number" ? value : Number(value); return Number.isFinite(number) ? number : null; } + function isAlphaNumeric(value) { + return value !== void 0 && /[A-Za-z0-9]/.test(value); + } + function isGrid(value) { + const normalized = value.trim().toUpperCase(); + return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR"; + } + function escapeFtxHtml(input) { + return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + } + function extractFtxGrids(message) { + return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))]; + } + function tokenize(message) { + return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean); + } + function isCallsign(token) { + return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token); + } + function extractFtxLocatorDetails(message) { + const tokens = tokenize(message); + const grids = extractFtxGrids(message); + const gridIndex = tokens.findIndex(isGrid); + const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign); + const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? ""); + const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null; + const target = directed ? callsigns[0] ?? null : null; + return grids.map((grid) => ({ grid, station: source, source, target })); + } + function extractFtxCallsign(message) { + return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null; + } + function renderFtxMessage(message) { + let html = ""; + let index = 0; + while (index < message.length) { + if (!isAlphaNumeric(message[index])) { + html += escapeFtxHtml(message[index] ?? ""); + index += 1; + continue; + } + let end = index + 1; + while (end < message.length && isAlphaNumeric(message[end])) end += 1; + const token = message.slice(index, end); + const grid = token.toUpperCase(); + html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token); + index = end; + } + return html; + } function initializeFtxDecoder(config) { - const { id, label, periodMs } = config; + const { id, label, periodMs, periodDigits = 1 } = config; const status = document.getElementById(`${id}-status`); const period = document.getElementById(`${id}-period`); const messagesElement = document.getElementById(`${id}-messages`); @@ -15,9 +65,7 @@ let filterText = ""; let history = []; const renderMessage = (message) => { - if (bridge.renderFt8Message) return bridge.renderFt8Message(message); - if (bridge.ft8EscapeHtml) return bridge.ft8EscapeHtml(message); - return message; + return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message); }; const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5; const prune = () => { @@ -69,9 +117,9 @@ }; const normalize = (message) => { const raw = message.message ?? ""; - const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? []; - const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? []; - const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? null; + const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw); + const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw); + const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw); const frequency = displayFrequency(message.freq_hz); if (grids.length > 0) { bridge.mapAddLocator?.(raw, grids, id, station, { @@ -103,6 +151,7 @@ history = []; bridge.updateFt8Bar?.(); render(); + bridge.clearMapMarkersByType?.(id); }; const barFrames = () => { const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8); @@ -133,7 +182,7 @@ }; bridge.registerFt8FamilyBarRenderer?.(id, barFrames); const updatePeriod = () => { - if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(1)}s`; + if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; }; updatePeriod(); window.setInterval(updatePeriod, 250); @@ -141,6 +190,14 @@ filterText = filterInput.value.trim().toUpperCase(); render(); }); + messagesElement?.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) return; + const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid; + if (grid) { + bridge.navigateToMapLocator?.(grid.toUpperCase(), id); + event.preventDefault(); + } + }); const toggle = document.getElementById(`${id}-decode-toggle-btn`); toggle?.addEventListener("click", () => { void (async () => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js index 4c03f942..dca9347f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js @@ -6,8 +6,58 @@ const number = typeof value === "number" ? value : Number(value); return Number.isFinite(number) ? number : null; } + function isAlphaNumeric(value) { + return value !== void 0 && /[A-Za-z0-9]/.test(value); + } + function isGrid(value) { + const normalized = value.trim().toUpperCase(); + return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR"; + } + function escapeFtxHtml(input) { + return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + } + function extractFtxGrids(message) { + return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))]; + } + function tokenize(message) { + return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean); + } + function isCallsign(token) { + return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token); + } + function extractFtxLocatorDetails(message) { + const tokens = tokenize(message); + const grids = extractFtxGrids(message); + const gridIndex = tokens.findIndex(isGrid); + const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign); + const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? ""); + const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null; + const target = directed ? callsigns[0] ?? null : null; + return grids.map((grid) => ({ grid, station: source, source, target })); + } + function extractFtxCallsign(message) { + return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null; + } + function renderFtxMessage(message) { + let html = ""; + let index = 0; + while (index < message.length) { + if (!isAlphaNumeric(message[index])) { + html += escapeFtxHtml(message[index] ?? ""); + index += 1; + continue; + } + let end = index + 1; + while (end < message.length && isAlphaNumeric(message[end])) end += 1; + const token = message.slice(index, end); + const grid = token.toUpperCase(); + html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token); + index = end; + } + return html; + } function initializeFtxDecoder(config) { - const { id, label, periodMs } = config; + const { id, label, periodMs, periodDigits = 1 } = config; const status = document.getElementById(`${id}-status`); const period = document.getElementById(`${id}-period`); const messagesElement = document.getElementById(`${id}-messages`); @@ -15,9 +65,7 @@ let filterText = ""; let history = []; const renderMessage = (message) => { - if (bridge.renderFt8Message) return bridge.renderFt8Message(message); - if (bridge.ft8EscapeHtml) return bridge.ft8EscapeHtml(message); - return message; + return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message); }; const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5; const prune = () => { @@ -69,9 +117,9 @@ }; const normalize = (message) => { const raw = message.message ?? ""; - const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? []; - const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? []; - const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? null; + const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw); + const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw); + const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw); const frequency = displayFrequency(message.freq_hz); if (grids.length > 0) { bridge.mapAddLocator?.(raw, grids, id, station, { @@ -103,6 +151,7 @@ history = []; bridge.updateFt8Bar?.(); render(); + bridge.clearMapMarkersByType?.(id); }; const barFrames = () => { const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8); @@ -133,7 +182,7 @@ }; bridge.registerFt8FamilyBarRenderer?.(id, barFrames); const updatePeriod = () => { - if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(1)}s`; + if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; }; updatePeriod(); window.setInterval(updatePeriod, 250); @@ -141,6 +190,14 @@ filterText = filterInput.value.trim().toUpperCase(); render(); }); + messagesElement?.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) return; + const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid; + if (grid) { + bridge.navigateToMapLocator?.(grid.toUpperCase(), id); + event.preventDefault(); + } + }); const toggle = document.getElementById(`${id}-decode-toggle-btn`); toggle?.addEventListener("click", () => { void (async () => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js index b00bae6e..8aa5c587 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js @@ -1,425 +1,271 @@ "use strict"; -const ft8Status = document.getElementById("ft8-status"); -const ft8PeriodEl = document.getElementById("ft8-period"); -const ft8MessagesEl = document.getElementById("ft8-messages"); -const ft8FilterInput = document.getElementById("ft8-filter"); -const ft8BarOverlay = document.getElementById("ft8-bar-overlay"); -const FT8_BAR_WINDOW_MS = 15 * 60 * 1e3; -const FT8_PERIOD_SECONDS = 15; -const FT8_MAX_DOM_ROWS = 200; -const FT8_BAR_DECODER_LABELS = { - ft8: "FT8", - ft4: "FT4", - ft2: "FT2" -}; -let ft8FilterText = ""; -let ft8MessageHistory = []; -let ft8BarActiveDecoder = "ft8"; -const ft8BarBuilders = {}; -const ft8BarDismissedAtMsByDecoder = { - ft8: 0, - ft4: 0, - ft2: 0 -}; -function currentFt8HistoryRetentionMs() { - return typeof window.getDecodeHistoryRetentionMs === "function" ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; -} -function pruneFt8MessageHistory() { - const cutoffMs = Date.now() - currentFt8HistoryRetentionMs(); - ft8MessageHistory = ft8MessageHistory.filter((msg) => Number(msg?._tsMs ?? msg?.ts_ms) >= cutoffMs); -} -function scheduleFt8Ui(key, job) { - if (typeof window.trxScheduleUiFrameJob === "function") { - window.trxScheduleUiFrameJob(key, job); - return; +(() => { + // src/plugins/ftx-family.ts + var bridge = window; + function finiteNumber(value) { + const number = typeof value === "number" ? value : Number(value); + return Number.isFinite(number) ? number : null; } - job(); -} -function scheduleFt8HistoryRender() { - scheduleFt8Ui("ft8-history", () => renderFt8History()); -} -function scheduleFt8BarUpdate() { - scheduleFt8Ui("ft8-bar", () => updateFt8Bar()); -} -window.registerFt8FamilyBarRenderer = function(decoder, builder) { - if (!FT8_BAR_DECODER_LABELS[decoder] || typeof builder !== "function") return; - ft8BarBuilders[decoder] = builder; -}; -window.setFt8FamilyBarDecoder = function(decoder) { - if (!FT8_BAR_DECODER_LABELS[decoder]) return; - ft8BarActiveDecoder = decoder; - scheduleFt8BarUpdate(); -}; -function normalizeFt8DisplayFreqHz(freqHz) { - const rawHz = Number(freqHz); - if (!Number.isFinite(rawHz)) return null; - const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null; - if (Number.isFinite(baseHz) && baseHz > 0 && rawHz >= 0 && rawHz < 1e5) { - return baseHz + rawHz; + function isAlphaNumeric(value) { + return value !== void 0 && /[A-Za-z0-9]/.test(value); } - return rawHz; -} -function fmtTime(tsMs) { - if (!tsMs) return "--:--:--"; - return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); -} -function updateFt8PeriodTimer() { - if (!ft8PeriodEl) return; - const nowSec = Math.floor(Date.now() / 1e3); - const remaining = FT8_PERIOD_SECONDS - nowSec % FT8_PERIOD_SECONDS; - ft8PeriodEl.textContent = `Next slot ${String(remaining).padStart(2, "0")}s`; -} -updateFt8PeriodTimer(); -setInterval(updateFt8PeriodTimer, 500); -function renderFt8Row(msg) { - const row = document.createElement("div"); - 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) : "--"; - const displayFreqHz = normalizeFt8DisplayFreqHz(msg.freq_hz); - const freq = Number.isFinite(displayFreqHz) ? displayFreqHz.toFixed(0) : "--"; - const renderedMessage = renderFt8Message(rawMessage); - row.innerHTML = `${fmtTime(msg.ts_ms)}${snr}${dt}${freq}${renderedMessage}`; - applyFt8FilterToRow(row); - return row; -} -function renderFt8History() { - pruneFt8MessageHistory(); - if (!ft8MessagesEl) return; - const fragment = document.createDocumentFragment(); - const limit = Math.min(ft8MessageHistory.length, FT8_MAX_DOM_ROWS); - for (let i = 0; i < limit; i += 1) { - fragment.appendChild(renderFt8Row(ft8MessageHistory[i])); + function isGrid(value) { + const normalized = value.trim().toUpperCase(); + return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR"; } - ft8MessagesEl.replaceChildren(fragment); -} -function addFt8Message(msg) { - msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now(); - ft8MessageHistory.unshift(msg); - pruneFt8MessageHistory(); - ft8BarActiveDecoder = "ft8"; - scheduleFt8BarUpdate(); - scheduleFt8HistoryRender(); -} -function normalizeServerFt8Message(msg) { - const raw = (msg.message || "").toString(); - const locatorDetails = ft8ExtractLocatorDetails(raw); - const grids = locatorDetails.length > 0 ? locatorDetails.map((detail) => detail.grid) : ft8ExtractAllGrids(raw); - const station = ft8ExtractLikelyCallsign(raw); - const rfHz = normalizeFt8DisplayFreqHz(msg.freq_hz); - return { - raw, - grids, - station, - rfHz, - locatorDetails, - history: { - receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null, - ts_ms: msg.ts_ms, - snr_db: msg.snr_db, - dt_s: msg.dt_s, - freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz, - message: msg.message + function escapeFtxHtml(input) { + return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + } + function extractFtxGrids(message) { + return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))]; + } + function tokenize(message) { + return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean); + } + function isCallsign(token) { + return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token); + } + function extractFtxLocatorDetails(message) { + const tokens = tokenize(message); + const grids = extractFtxGrids(message); + const gridIndex = tokens.findIndex(isGrid); + const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign); + const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? ""); + const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null; + const target = directed ? callsigns[0] ?? null : null; + return grids.map((grid) => ({ grid, station: source, source, target })); + } + function extractFtxCallsign(message) { + return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null; + } + function renderFtxMessage(message) { + let html = ""; + let index = 0; + while (index < message.length) { + if (!isAlphaNumeric(message[index])) { + html += escapeFtxHtml(message[index] ?? ""); + index += 1; + continue; + } + let end = index + 1; + while (end < message.length && isAlphaNumeric(message[end])) end += 1; + const token = message.slice(index, end); + const grid = token.toUpperCase(); + html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token); + index = end; } - }; -} -window.onServerFt8Batch = function(messages) { - if (!Array.isArray(messages) || messages.length === 0) return; - ft8Status.textContent = "Receiving"; - const normalized = []; - for (const msg of messages) { - const next = normalizeServerFt8Message(msg); - if (next.grids.length > 0 && window.mapAddLocator) { - window.mapAddLocator(next.raw, next.grids, "ft8", next.station, { - ...msg, - freq_hz: next.rfHz, - locator_details: next.locatorDetails + return html; + } + function installFtxCompatibilityHelpers() { + bridge.renderFt8Message = renderFtxMessage; + bridge.ft8EscapeHtml = escapeFtxHtml; + bridge.ft8ExtractLocatorDetails = extractFtxLocatorDetails; + bridge.ft8ExtractAllGrids = extractFtxGrids; + bridge.ft8ExtractLikelyCallsign = extractFtxCallsign; + } + function initializeFt8FamilyBar() { + const labels = { ft8: "FT8", ft4: "FT4", ft2: "FT2" }; + const builders = {}; + const dismissed = { ft8: 0, ft4: 0, ft2: 0 }; + const overlay = document.getElementById("ft8-bar-overlay"); + let active = "ft8"; + const update = () => { + if (!overlay) return; + const mode = (document.getElementById("mode")?.value ?? "").toUpperCase(); + const result = builders[active]?.(); + if (mode !== "DIG" && mode !== "USB" || !result || result.count === 0 || result.newestTsMs <= dismissed[active]) { + overlay.style.display = "none"; + overlay.innerHTML = ""; + return; + } + const label = labels[active]; + overlay.innerHTML = `
${result.html}`; + overlay.style.display = "flex"; + }; + bridge.registerFt8FamilyBarRenderer = (decoder, builder) => { + builders[decoder] = builder; + }; + bridge.setFt8FamilyBarDecoder = (decoder) => { + active = decoder; + update(); + }; + bridge.updateFt8Bar = update; + bridge.clearFt8Bar = () => { + ({ ft8: bridge.resetFt8HistoryView, ft4: bridge.resetFt4HistoryView, ft2: bridge.resetFt2HistoryView })[active]?.(); + }; + bridge.closeFt8Bar = () => { + dismissed[active] = Date.now(); + update(); + }; + } + function initializeFtxDecoder(config) { + const { id, label, periodMs, periodDigits = 1 } = config; + const status = document.getElementById(`${id}-status`); + const period = document.getElementById(`${id}-period`); + const messagesElement = document.getElementById(`${id}-messages`); + const filterInput = document.getElementById(`${id}-filter`); + let filterText = ""; + let history = []; + const renderMessage = (message) => { + return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message); + }; + const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5; + const prune = () => { + const cutoff = Date.now() - retentionMs(); + history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff); + }; + const schedule = (job) => { + if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job); + else job(); + }; + const displayFrequency = (value) => { + const raw = finiteNumber(value); + if (raw === null) return null; + const base = finiteNumber(bridge.ft8BaseHz); + return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw; + }; + const renderRow = (message) => { + const row = document.createElement("div"); + row.className = "ft8-row"; + const raw = message.message ?? ""; + row.dataset.message = raw.toUpperCase(); + row.dataset.decoder = id; + const storedFrequency = finiteNumber(message.freq_hz); + row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency); + const snr = finiteNumber(message.snr_db); + const delta = finiteNumber(message.dt_s); + const frequency = displayFrequency(message.freq_hz); + const timestamp = finiteNumber(message._tsMs ?? message.ts_ms); + const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" }); - } - next.history._tsMs = Number.isFinite(next.history?.ts_ms) ? Number(next.history.ts_ms) : Date.now(); - normalized.push(next.history); - } - normalized.reverse(); - ft8MessageHistory = normalized.concat(ft8MessageHistory); - pruneFt8MessageHistory(); - ft8BarActiveDecoder = "ft8"; - scheduleFt8BarUpdate(); - scheduleFt8HistoryRender(); -}; -window.restoreFt8History = function(messages) { - window.onServerFt8Batch(messages); -}; -window.pruneFt8HistoryView = function() { - pruneFt8MessageHistory(); - updateFt8Bar(); - renderFt8History(); -}; -function ft8BarRfText(msg) { - const displayFreqHz = normalizeFt8DisplayFreqHz(msg.freq_hz); - if (!Number.isFinite(displayFreqHz)) return null; - return `${displayFreqHz.toFixed(0)} Hz`; -} -function buildFt8BarFrames() { - const cutoffMs = Date.now() - FT8_BAR_WINDOW_MS; - const messages = ft8MessageHistory.filter((msg) => Number(msg.ts_ms) >= cutoffMs).slice(0, 8); - const newestTsMs = messages.reduce((latest, msg) => Math.max(latest, Number(msg.ts_ms) || 0), 0); - if (messages.length === 0) { - return { count: 0, newestTsMs: 0, html: "" }; - } - let html = ""; - for (const msg of messages) { - const ts = msg.ts_ms ? `` : ""; - const snr = Number.isFinite(msg.snr_db) ? `${msg.snr_db.toFixed(1)} dB` : "-- dB"; - const dt = Number.isFinite(msg.dt_s) ? `dt ${msg.dt_s.toFixed(2)}` : null; - const rf = ft8BarRfText(msg); - const detail = [snr, dt, rf].filter(Boolean).join(" · "); - const text = ft8EscapeHtml((msg.message || "").toString()); - html += ``; - } - return { count: messages.length, newestTsMs, html }; -} -function updateFt8Bar() { - if (!ft8BarOverlay) return; - const modeUpper = (document.getElementById("mode")?.value || "").toUpperCase(); - const isFt8Mode = modeUpper === "DIG" || modeUpper === "USB"; - const decoder = ft8BarActiveDecoder; - const builder = ft8BarBuilders[decoder]; - const label = FT8_BAR_DECODER_LABELS[decoder] || "FT8"; - const result = typeof builder === "function" ? builder() : null; - const newestTsMs = Number(result?.newestTsMs) || 0; - if (!isFt8Mode || !result || result.count === 0 || newestTsMs <= (ft8BarDismissedAtMsByDecoder[decoder] || 0)) { - ft8BarOverlay.style.display = "none"; - ft8BarOverlay.innerHTML = ""; - return; - } - ft8BarOverlay.innerHTML = `${result.html}`; - ft8BarOverlay.style.display = "flex"; -} -window.updateFt8Bar = updateFt8Bar; -window.clearFt8Bar = function() { - const decoder = ft8BarActiveDecoder; - if (decoder === "ft4") { - window.resetFt4HistoryView?.(); - return; - } - if (decoder === "ft2") { - window.resetFt2HistoryView?.(); - return; - } - window.resetFt8HistoryView?.(); -}; -window.closeFt8Bar = function() { - ft8BarDismissedAtMsByDecoder[ft8BarActiveDecoder] = Date.now(); - if (ft8BarOverlay) { - ft8BarOverlay.style.display = "none"; - ft8BarOverlay.innerHTML = ""; - } -}; -window.registerFt8FamilyBarRenderer("ft8", buildFt8BarFrames); -function renderFt8Message(message) { - let out = ""; - let i = 0; - while (i < message.length) { - const ch = message[i]; - if (ft8IsAlphaNum(ch)) { - let j = i + 1; - while (j < message.length && ft8IsAlphaNum(message[j])) j++; - const token = message.slice(i, j); - const grid = token.toUpperCase(); - if (ft8IsMaidenheadGridToken(grid)) { - out += `${grid}`; - } else { - out += ft8EscapeHtml(token); + row.innerHTML = `${time}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${frequency?.toFixed(0) ?? "--"}${renderMessage(raw)}`; + return row; + }; + const render = () => { + prune(); + if (!messagesElement) return; + const fragment = document.createDocumentFragment(); + let count = 0; + for (const message of history) { + if (count >= 200) break; + if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue; + fragment.appendChild(renderRow(message)); + count += 1; } - i = j; - } else { - out += ft8EscapeHtml(ch); - i += 1; - } - } - return out; -} -function ft8TokenizeMessage(message) { - return String(message || "").toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean); -} -function ft8ExtractAllGrids(message) { - const out = []; - const seen = /* @__PURE__ */ new Set(); - let i = 0; - while (i < message.length) { - if (ft8IsAlphaNum(message[i])) { - let j = i + 1; - while (j < message.length && ft8IsAlphaNum(message[j])) j++; - const token = message.slice(i, j); - const grid = token.toUpperCase(); - if (ft8IsMaidenheadGridToken(grid) && !seen.has(grid)) { - seen.add(grid); - out.push(grid); + messagesElement.replaceChildren(fragment); + }; + const normalize = (message) => { + const raw = message.message ?? ""; + const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw); + const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw); + const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw); + const frequency = displayFrequency(message.freq_hz); + if (grids.length > 0) { + bridge.mapAddLocator?.(raw, grids, id, station, { + ...message, + freq_hz: frequency ?? message.freq_hz, + locator_details: locatorDetails + }); } - i = j; - } else { - i += 1; - } - } - return out; -} -function ft8ExtractLocatorDetails(message) { - const tokens = ft8TokenizeMessage(message); - const grids = ft8ExtractAllGrids(String(message || "")); - if (tokens.length === 0 || grids.length === 0) return []; - const firstGridIdx = tokens.findIndex((token) => ft8IsMaidenheadGridToken(token)); - const limit = firstGridIdx >= 0 ? firstGridIdx : tokens.length; - const callsigns = []; - for (let i = 0; i < limit; i += 1) { - if (ft8IsLikelyCallsignToken(tokens[i])) callsigns.push(tokens[i]); - } - let source = null; - let target = null; - const head = tokens[0]; - if (callsigns.length > 0) { - if (head === "CQ" || head === "DE" || head === "QRZ") { - source = callsigns[0]; - } else if (callsigns.length >= 2) { - target = callsigns[0]; - source = callsigns[1]; - } else { - source = callsigns[0]; - } - } - return grids.map((grid) => ({ - grid, - station: source || null, - source: source || null, - target: target || null - })); -} -function ft8ExtractLikelyCallsign(message) { - const locatorDetails = ft8ExtractLocatorDetails(message); - if (locatorDetails.length > 0 && locatorDetails[0].station) { - return locatorDetails[0].station; - } - const tokens = ft8TokenizeMessage(message); - for (const token of tokens) { - if (ft8IsLikelyCallsignToken(token)) return token; - } - return null; -} -function ft8IsLikelyCallsignToken(token) { - if (!token) return false; - if (token.length < 3 || token.length > 12) return false; - if (token === "CQ" || token === "DE" || token === "QRZ" || token === "DX") return false; - if (ft8IsMaidenheadGridToken(token)) return false; - return /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token); -} -function ft8IsFarewellToken(token) { - const normalized = String(token || "").trim().toUpperCase(); - return normalized === "RR73" || normalized === "73" || normalized === "RR"; -} -function ft8IsMaidenheadGridToken(token) { - const normalized = String(token || "").trim().toUpperCase(); - return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !ft8IsFarewellToken(normalized); -} -function ft8EscapeHtml(input) { - return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); -} -function ft8IsAlphaNum(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 = ""; - return; - } - const message = row.dataset.message || ""; - row.style.display = message.includes(ft8FilterText) ? "" : "none"; -} -function applyFt8FilterToAll() { - const rows = ft8MessagesEl.querySelectorAll(".ft8-row"); - rows.forEach((row) => applyFt8FilterToRow(row)); -} -function updateFt8RowRf(row) { - const freqEl = row.querySelector(".ft8-freq"); - if (!freqEl) return; - const storedFreqHz = row.dataset.storedFreqHz ? Number(row.dataset.storedFreqHz) : NaN; - const displayFreqHz = normalizeFt8DisplayFreqHz(storedFreqHz); - if (Number.isFinite(displayFreqHz)) { - freqEl.textContent = displayFreqHz.toFixed(0); - } else { - freqEl.textContent = "--"; - } -} -window.updateFt8RfDisplay = function() { - const rows = ft8MessagesEl.querySelectorAll(".ft8-row"); - rows.forEach((row) => updateFt8RowRf(row)); - updateFt8Bar(); -}; -window.resetFt8HistoryView = function() { - ft8MessagesEl.innerHTML = ""; - ft8MessageHistory = []; - updateFt8Bar(); - renderFt8History(); - if (window.clearMapMarkersByType) window.clearMapMarkersByType("ft8"); -}; -if (ft8FilterInput) { - ft8FilterInput.addEventListener("input", () => { - ft8FilterText = ft8FilterInput.value.trim().toUpperCase(); - renderFt8History(); - }); -} -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(); - }); -} -const ft8DecodeToggleBtn = document.getElementById("ft8-decode-toggle-btn"); -ft8DecodeToggleBtn?.addEventListener("click", async () => { - try { - await window.takeSchedulerControlForDecoderDisable?.(ft8DecodeToggleBtn); - await postPath("/toggle_ft8_decode"); - } catch (e) { - console.error("FT8 toggle failed", e); - } -}); -document.getElementById("settings-clear-ft8-history")?.addEventListener("click", async () => { - if (!await window.trxUi.confirm({ title: "Clear FT8 history?", message: "All stored FT8 decodes will be permanently removed.", confirmLabel: "Clear history" })) return; - try { - await postPath("/clear_ft8_decode"); - window.resetFt8HistoryView(); - } catch (e) { - console.error("FT8 history clear failed", e); - } -}); -window.onServerFt8 = function(msg) { - ft8Status.textContent = "Receiving"; - const next = normalizeServerFt8Message(msg); - if (next.grids.length > 0 && window.mapAddLocator) { - window.mapAddLocator(next.raw, next.grids, "ft8", next.station, { - ...msg, - freq_hz: next.rfHz, - locator_details: next.locatorDetails + return { + receiver: bridge.getDecodeRigMeta?.() ?? null, + ts_ms: message.ts_ms, + snr_db: message.snr_db, + dt_s: message.dt_s, + freq_hz: frequency ?? message.freq_hz, + message: message.message, + _tsMs: finiteNumber(message.ts_ms) ?? Date.now() + }; + }; + const receiveBatch = (messages) => { + if (messages.length === 0) return; + if (status) status.textContent = "Receiving"; + history = messages.map(normalize).reverse().concat(history); + prune(); + bridge.setFt8FamilyBarDecoder?.(id); + bridge.updateFt8Bar?.(); + schedule(render); + }; + const reset = () => { + history = []; + bridge.updateFt8Bar?.(); + render(); + bridge.clearMapMarkersByType?.(id); + }; + const barFrames = () => { + const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8); + let html = ""; + for (const message of recent) { + const timestamp = finiteNumber(message._tsMs ?? message.ts_ms); + const time = timestamp === null ? "" : ``; + const snr = finiteNumber(message.snr_db); + const delta = finiteNumber(message.dt_s); + const frequency = displayFrequency(message.freq_hz); + const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · "); + html += ``; + } + return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html }; + }; + const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; + bridge[`onServer${capitalized}Batch`] = (messages) => { + if (Array.isArray(messages)) receiveBatch(messages); + }; + bridge[`restore${capitalized}History`] = receiveBatch; + bridge[`prune${capitalized}HistoryView`] = () => { + prune(); + render(); + }; + bridge[`reset${capitalized}HistoryView`] = reset; + bridge[`onServer${capitalized}`] = (message) => { + receiveBatch([message]); + }; + bridge.registerFt8FamilyBarRenderer?.(id, barFrames); + const updatePeriod = () => { + if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; + }; + updatePeriod(); + window.setInterval(updatePeriod, 250); + filterInput?.addEventListener("input", () => { + filterText = filterInput.value.trim().toUpperCase(); + render(); + }); + messagesElement?.addEventListener("click", (event) => { + if (!(event.target instanceof Element)) return; + const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid; + if (grid) { + bridge.navigateToMapLocator?.(grid.toUpperCase(), id); + event.preventDefault(); + } + }); + const toggle = document.getElementById(`${id}-decode-toggle-btn`); + toggle?.addEventListener("click", () => { + void (async () => { + try { + await bridge.takeSchedulerControlForDecoderDisable?.(toggle); + await bridge.postPath?.(`/toggle_${id}_decode`); + } catch (error) { + console.error(`${label} toggle failed`, error); + } + })(); + }); + document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => { + void (async () => { + if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return; + try { + await bridge.postPath?.(`/clear_${id}_decode`); + reset(); + } catch (error) { + console.error(`${label} history clear failed`, error); + } + })(); }); } - addFt8Message(next.history); -}; + + // src/plugins/ft8.ts + installFtxCompatibilityHelpers(); + initializeFt8FamilyBar(); + initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 }); +})(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs index d929c723..c73bd27f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs @@ -26,7 +26,6 @@ await build({ aprs: path.join(sourceDir, "plugins", "aprs.js"), "background-decode": path.join(sourceDir, "plugins", "background-decode.js"), bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"), - ft8: path.join(sourceDir, "plugins", "ft8.js"), "hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.js"), sat: path.join(sourceDir, "plugins", "sat.js"), "sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"), @@ -50,6 +49,7 @@ await build({ ft4: path.join(sourceDir, "plugins", "ft4.ts"), wspr: path.join(sourceDir, "plugins", "wspr.ts"), cw: path.join(sourceDir, "plugins", "cw.ts"), + ft8: path.join(sourceDir, "plugins", "ft8.ts"), }, outdir: outputDir, bundle: true, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ft8.js deleted file mode 100644 index c9c4edff..00000000 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ft8.js +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stan Grams