diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js index aab2aeb9..99fa5838 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js @@ -10,7 +10,7 @@ const pluginGroups = { }; const loaded = /* @__PURE__ */ new Set(); const loading = /* @__PURE__ */ new Map(); -const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js"]); +const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]); function loadLegacyScript(path) { return new Promise((resolve, reject) => { const script = document.createElement("script"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js index 73b4eda3..abad6484 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js @@ -1,282 +1,267 @@ "use strict"; -const vdesStatus = document.getElementById("vdes-status"); -const vdesMessagesEl = document.getElementById("vdes-messages"); -const vdesFilterInput = document.getElementById("vdes-filter"); -const vdesBarOverlay = document.getElementById("vdes-bar-overlay"); -const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary"); -const vdesFrameCountEl = document.getElementById("vdes-frame-count"); -const vdesLatestSeenEl = document.getElementById("vdes-latest-seen"); -const VDES_BAR_WINDOW_MS = 15 * 60 * 1e3; -let vdesFilterText = ""; -let vdesMessageHistory = []; -function currentVdesHistoryRetentionMs() { - return typeof window.getDecodeHistoryRetentionMs === "function" ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; -} -function pruneVdesMessageHistory() { - const cutoffMs = Date.now() - currentVdesHistoryRetentionMs(); - vdesMessageHistory = vdesMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs); -} -function scheduleVdesUi(key, job) { - if (typeof window.trxScheduleUiFrameJob === "function") { - window.trxScheduleUiFrameJob(key, job); - return; +(() => { + // src/plugins/vdes.ts + var vdesWindow = window; + var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + var vdesStatus = document.getElementById("vdes-status"); + var vdesMessagesEl = document.getElementById("vdes-messages"); + var vdesFilterInput = document.getElementById("vdes-filter"); + var vdesBarOverlay = document.getElementById("vdes-bar-overlay"); + var vdesChannelSummaryEl = document.getElementById("vdes-channel-summary"); + var vdesFrameCountEl = document.getElementById("vdes-frame-count"); + var vdesLatestSeenEl = document.getElementById("vdes-latest-seen"); + var VDES_BAR_WINDOW_MS = 15 * 60 * 1e3; + var vdesFilterText = ""; + var vdesMessageHistory = []; + function currentVdesHistoryRetentionMs() { + return typeof vdesWindow.getDecodeHistoryRetentionMs === "function" ? vdesWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; } - job(); -} -function scheduleVdesHistoryRender() { - scheduleVdesUi("vdes-history", () => renderVdesHistory()); -} -function scheduleVdesBarUpdate() { - scheduleVdesUi("vdes-bar", () => updateVdesBar()); -} -function currentVdesCenterText() { - const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, ""); - const hz = raw ? Number(raw) : 0; - if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency"; - return `100 kHz @ ${(hz / 1e6).toFixed(3)} MHz`; -} -function vdesAgeText(tsMs) { - if (!Number.isFinite(tsMs)) return "just now"; - const deltaMs = Math.max(0, Date.now() - tsMs); - const seconds = Math.round(deltaMs / 1e3); - if (seconds < 5) return "just now"; - if (seconds < 60) return `${seconds}s ago`; - const minutes = Math.round(seconds / 60); - if (minutes < 60) return `${minutes}m ago`; - const hours = Math.round(minutes / 60); - return `${hours}h ago`; -} -function vdesHexPreview(rawBytes) { - if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--"; - return rawBytes.slice(0, 20).map((value) => Number(value).toString(16).padStart(2, "0")).join(" ").toUpperCase(); -} -function updateVdesSummary() { - pruneVdesMessageHistory(); - if (vdesChannelSummaryEl) { - vdesChannelSummaryEl.textContent = currentVdesCenterText(); + function pruneVdesMessageHistory() { + const cutoffMs = Date.now() - currentVdesHistoryRetentionMs(); + vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs); } - if (vdesFrameCountEl) { - const count = vdesMessageHistory.length; - vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`; + function scheduleVdesUi(key, job) { + if (typeof vdesWindow.trxScheduleUiFrameJob === "function") { + vdesWindow.trxScheduleUiFrameJob(key, job); + return; + } + job(); } - if (vdesLatestSeenEl) { - const latest = vdesMessageHistory[0]; - vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet"; + function scheduleVdesHistoryRender() { + scheduleVdesUi("vdes-history", () => { + renderVdesHistory(); + }); } -} -function applyVdesFilterToRow(row) { - if (!vdesFilterText) { - row.style.display = ""; - return; + function scheduleVdesBarUpdate() { + scheduleVdesUi("vdes-bar", () => { + updateVdesBar(); + }); } - const text = row.dataset.filterText || ""; - row.style.display = text.includes(vdesFilterText) ? "" : "none"; -} -function applyVdesFilterToAll() { - if (!vdesMessagesEl) return; - vdesMessagesEl.querySelectorAll(".vdes-message").forEach((row) => applyVdesFilterToRow(row)); -} -function renderVdesRow(msg) { - const row = document.createElement("div"); - row.className = "vdes-message"; - const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - }); - const title = msg.vessel_name || "VDES Burst"; - const label = msg.callsign || "VDES"; - const info = msg.destination || ""; - const labelText = msg.message_label || ""; - const linkText = Number.isFinite(msg.link_id) ? `LID ${msg.link_id}` : ""; - const syncText = Number.isFinite(msg.sync_score) ? `Sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : ""; - const phaseText = Number.isFinite(msg.phase_rotation) ? `R${Number(msg.phase_rotation)}` : ""; - const fecText = msg.fec_state || ""; - const srcText = Number.isFinite(msg.source_id) ? `SRC ${Number(msg.source_id)}` : ""; - const dstText = Number.isFinite(msg.destination_id) ? `DST ${Number(msg.destination_id)}` : ""; - const sessionText = Number.isFinite(msg.session_id) ? `S${Number(msg.session_id)}` : ""; - const asmText = Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : ""; - const countText = Number.isFinite(msg.data_count) ? `${Number(msg.data_count)} data bits` : ""; - const ackText = Number.isFinite(msg.ack_nack_mask) ? `ACK 0x${Number(msg.ack_nack_mask).toString(16).toUpperCase().padStart(4, "0")}` : ""; - const cqiText = Number.isFinite(msg.channel_quality) ? `CQ ${Number(msg.channel_quality)}` : ""; - const previewText = msg.payload_preview || ""; - const rawHex = vdesHexPreview(msg.raw_bytes); - row.dataset.filterText = [ - title, - label, - labelText, - info, - srcText, - dstText, - sessionText, - asmText, - countText, - ackText, - cqiText, - previewText, - linkText, - syncText, - phaseText, - fecText, - rawHex, - msg.message_type, - msg.bit_len - ].filter(Boolean).join(" ").toUpperCase(); - row.innerHTML = `
${ts}${escapeMapHtml(title)}${escapeMapHtml(label)}` + (labelText ? `${escapeMapHtml(labelText)}` : "") + (linkText ? `${escapeMapHtml(linkText)}` : "") + (srcText ? `${escapeMapHtml(srcText)}` : "") + (dstText ? `${escapeMapHtml(dstText)}` : "") + (syncText ? `${escapeMapHtml(syncText)}` : "") + (phaseText ? `${escapeMapHtml(phaseText)}` : "") + `T${escapeMapHtml(String(msg.message_type ?? "--"))}
${escapeMapHtml(currentVdesCenterText())}${escapeMapHtml(`${msg.bit_len || 0} bits`)}` + (sessionText ? `${escapeMapHtml(sessionText)}` : "") + (asmText ? `${escapeMapHtml(asmText)}` : "") + (countText ? `${escapeMapHtml(countText)}` : "") + (ackText ? `${escapeMapHtml(ackText)}` : "") + (cqiText ? `${escapeMapHtml(cqiText)}` : "") + (info ? `${escapeMapHtml(info)}` : "") + (fecText ? `${escapeMapHtml(fecText)}` : "") + `${escapeMapHtml(vdesAgeText(msg._tsMs))}
` + (previewText ? `${escapeMapHtml(previewText)}` : "") + (previewText ? `·` : "") + `${escapeMapHtml(rawHex)}
`; - applyVdesFilterToRow(row); - return row; -} -function updateVdesBar() { - if (!vdesBarOverlay) return; - updateVdesSummary(); - const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES"; - const cutoffMs = Date.now() - VDES_BAR_WINDOW_MS; - const messages = vdesMessageHistory.filter((msg) => msg._tsMs >= cutoffMs).slice(0, 6); - if (!isVdes || messages.length === 0) { - vdesBarOverlay.style.display = "none"; - vdesBarOverlay.innerHTML = ""; - return; + function currentVdesCenterText() { + const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, ""); + const hz = raw ? Number(raw) : 0; + if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency"; + return `100 kHz @ ${(hz / 1e6).toFixed(3)} MHz`; } - let html = `
VDESLiveClearLast 15 minutes
`; - for (const msg of messages) { - const ts = msg._ts ? `${msg._ts}` : ""; - const label = escapeMapHtml(msg.callsign || "VDES"); - const title = escapeMapHtml(msg.vessel_name || "Burst"); - const detail = [ - `${msg.bit_len || 0} bits`, - msg.message_label ? escapeMapHtml(msg.message_label) : null, - Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null, - Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null, - Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null, - Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null, - Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null, - Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null, - msg.destination ? escapeMapHtml(msg.destination) : null, - escapeMapHtml(vdesAgeText(msg._tsMs)) - ].filter(Boolean).join(" · "); - html += `
${ts}${title} ${label}: ${detail}
`; + function vdesAgeText(tsMs) { + if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now"; + const deltaMs = Math.max(0, Date.now() - tsMs); + const seconds = Math.round(deltaMs / 1e3); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.round(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.round(minutes / 60); + return `${hours}h ago`; } - vdesBarOverlay.innerHTML = html; - vdesBarOverlay.style.display = "flex"; -} -window.updateVdesBar = updateVdesBar; -window.clearVdesBar = function() { - window.resetVdesHistoryView(); -}; -window.resetVdesHistoryView = function() { - if (vdesMessagesEl) vdesMessagesEl.innerHTML = ""; - vdesMessageHistory = []; - updateVdesBar(); - renderVdesHistory(); -}; -function renderVdesHistory() { - pruneVdesMessageHistory(); - if (!vdesMessagesEl) { - updateVdesSummary(); - return; + function vdesHexPreview(rawBytes) { + if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--"; + return rawBytes.slice(0, 20).map((value) => value.toString(16).padStart(2, "0")).join(" ").toUpperCase(); } - const fragment = document.createDocumentFragment(); - for (let i = 0; i < vdesMessageHistory.length; i += 1) { - fragment.appendChild(renderVdesRow(vdesMessageHistory[i])); + function updateVdesSummary() { + pruneVdesMessageHistory(); + if (vdesChannelSummaryEl) { + vdesChannelSummaryEl.textContent = currentVdesCenterText(); + } + if (vdesFrameCountEl) { + const count = vdesMessageHistory.length; + vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`; + } + if (vdesLatestSeenEl) { + const latest = vdesMessageHistory[0]; + vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet"; + } } - vdesMessagesEl.replaceChildren(fragment); - updateVdesSummary(); -} -function addVdesMessage(msg) { - const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); - msg._tsMs = tsMs; - msg._ts = new Date(tsMs).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit" - }); - vdesMessageHistory.unshift(msg); - pruneVdesMessageHistory(); - scheduleVdesBarUpdate(); - scheduleVdesHistoryRender(); -} -function normalizeServerVdesMessage(msg) { - return { - rig_id: msg.rig_id || null, - message_type: msg.message_type, - bit_len: msg.bit_len, - raw_bytes: msg.raw_bytes, - lat: msg.lat, - lon: msg.lon, - vessel_name: msg.vessel_name, - callsign: msg.callsign, - destination: msg.destination, - message_label: msg.message_label, - session_id: msg.session_id, - source_id: msg.source_id, - destination_id: msg.destination_id, - data_count: msg.data_count, - asm_identifier: msg.asm_identifier, - ack_nack_mask: msg.ack_nack_mask, - channel_quality: msg.channel_quality, - payload_preview: msg.payload_preview, - link_id: msg.link_id, - sync_score: msg.sync_score, - sync_errors: msg.sync_errors, - phase_rotation: msg.phase_rotation, - fec_state: msg.fec_state, - ts_ms: msg.ts_ms - }; -} -window.onServerVdesBatch = function(messages) { - if (!Array.isArray(messages) || messages.length === 0) return; - if (vdesStatus) vdesStatus.textContent = "Receiving"; - const normalized = []; - for (const msg of messages) { - const next = normalizeServerVdesMessage(msg); - const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now(); - next._tsMs = tsMs; - next._ts = new Date(tsMs).toLocaleTimeString([], { + function applyVdesFilterToRow(row) { + if (!vdesFilterText) { + row.style.display = ""; + return; + } + const text = row.dataset.filterText || ""; + row.style.display = text.includes(vdesFilterText) ? "" : "none"; + } + function renderVdesRow(msg) { + const row = document.createElement("div"); + row.className = "vdes-message"; + const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); - if (next.lat != null && next.lon != null && window.vdesMapAddPoint) { - window.vdesMapAddPoint(next); + const title = msg.vessel_name || "VDES Burst"; + const label = msg.callsign || "VDES"; + const info = msg.destination || ""; + const labelText = msg.message_label || ""; + const linkText = Number.isFinite(msg.link_id) ? `LID ${msg.link_id}` : ""; + const syncText = Number.isFinite(msg.sync_score) ? `Sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : ""; + const phaseText = Number.isFinite(msg.phase_rotation) ? `R${Number(msg.phase_rotation)}` : ""; + const fecText = msg.fec_state || ""; + const srcText = Number.isFinite(msg.source_id) ? `SRC ${Number(msg.source_id)}` : ""; + const dstText = Number.isFinite(msg.destination_id) ? `DST ${Number(msg.destination_id)}` : ""; + const sessionText = Number.isFinite(msg.session_id) ? `S${Number(msg.session_id)}` : ""; + const asmText = Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : ""; + const countText = Number.isFinite(msg.data_count) ? `${Number(msg.data_count)} data bits` : ""; + const ackText = Number.isFinite(msg.ack_nack_mask) ? `ACK 0x${Number(msg.ack_nack_mask).toString(16).toUpperCase().padStart(4, "0")}` : ""; + const cqiText = Number.isFinite(msg.channel_quality) ? `CQ ${Number(msg.channel_quality)}` : ""; + const previewText = msg.payload_preview || ""; + const rawHex = vdesHexPreview(msg.raw_bytes); + row.dataset.filterText = [ + title, + label, + labelText, + info, + srcText, + dstText, + sessionText, + asmText, + countText, + ackText, + cqiText, + previewText, + linkText, + syncText, + phaseText, + fecText, + rawHex, + msg.message_type, + msg.bit_len + ].filter(Boolean).join(" ").toUpperCase(); + row.innerHTML = `
${ts}${escapeVdesHtml(title)}${escapeVdesHtml(label)}` + (labelText ? `${escapeVdesHtml(labelText)}` : "") + (linkText ? `${escapeVdesHtml(linkText)}` : "") + (srcText ? `${escapeVdesHtml(srcText)}` : "") + (dstText ? `${escapeVdesHtml(dstText)}` : "") + (syncText ? `${escapeVdesHtml(syncText)}` : "") + (phaseText ? `${escapeVdesHtml(phaseText)}` : "") + `T${escapeVdesHtml(String(msg.message_type ?? "--"))}
${escapeVdesHtml(currentVdesCenterText())}${escapeVdesHtml(`${msg.bit_len || 0} bits`)}` + (sessionText ? `${escapeVdesHtml(sessionText)}` : "") + (asmText ? `${escapeVdesHtml(asmText)}` : "") + (countText ? `${escapeVdesHtml(countText)}` : "") + (ackText ? `${escapeVdesHtml(ackText)}` : "") + (cqiText ? `${escapeVdesHtml(cqiText)}` : "") + (info ? `${escapeVdesHtml(info)}` : "") + (fecText ? `${escapeVdesHtml(fecText)}` : "") + `${escapeVdesHtml(vdesAgeText(msg._tsMs))}
` + (previewText ? `${escapeVdesHtml(previewText)}` : "") + (previewText ? `·` : "") + `${escapeVdesHtml(rawHex)}
`; + applyVdesFilterToRow(row); + return row; + } + function updateVdesBar() { + if (!vdesBarOverlay) return; + updateVdesSummary(); + const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES"; + const cutoffMs = Date.now() - VDES_BAR_WINDOW_MS; + const messages = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs).slice(0, 6); + if (!isVdes || messages.length === 0) { + vdesBarOverlay.style.display = "none"; + vdesBarOverlay.innerHTML = ""; + return; } - normalized.push(next); + let html = `
VDESLiveClearLast 15 minutes
`; + for (const msg of messages) { + const ts = msg._ts ? `${msg._ts}` : ""; + const label = escapeVdesHtml(msg.callsign || "VDES"); + const title = escapeVdesHtml(msg.vessel_name || "Burst"); + const detail = [ + `${msg.bit_len || 0} bits`, + msg.message_label ? escapeVdesHtml(msg.message_label) : null, + Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null, + Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null, + Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null, + Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null, + Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null, + Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null, + msg.destination ? escapeVdesHtml(msg.destination) : null, + escapeVdesHtml(vdesAgeText(msg._tsMs)) + ].filter(Boolean).join(" · "); + html += `
${ts}${title} ${label}: ${detail}
`; + } + vdesBarOverlay.innerHTML = html; + vdesBarOverlay.style.display = "flex"; } - normalized.reverse(); - vdesMessageHistory = normalized.concat(vdesMessageHistory); - pruneVdesMessageHistory(); - scheduleVdesBarUpdate(); - scheduleVdesHistoryRender(); -}; -window.restoreVdesHistory = function(messages) { - window.onServerVdesBatch(messages); -}; -document.getElementById("settings-clear-vdes-history")?.addEventListener("click", async () => { - if (!await window.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return; - try { - await postPath("/clear_vdes_decode"); - window.resetVdesHistoryView(); - } catch (e) { - console.error("VDES history clear failed", e); - } -}); -if (vdesFilterInput) { - vdesFilterInput.addEventListener("input", () => { - vdesFilterText = vdesFilterInput.value.trim().toUpperCase(); + vdesWindow.updateVdesBar = updateVdesBar; + vdesWindow.clearVdesBar = function() { + vdesWindow.resetVdesHistoryView?.(); + }; + vdesWindow.resetVdesHistoryView = function() { + if (vdesMessagesEl) vdesMessagesEl.innerHTML = ""; + vdesMessageHistory = []; + updateVdesBar(); renderVdesHistory(); - }); -} -window.onServerVdes = function(msg) { - if (vdesStatus) vdesStatus.textContent = "Receiving"; - const next = normalizeServerVdesMessage(msg); - addVdesMessage(next); - if (next.lat != null && next.lon != null && window.vdesMapAddPoint) { - window.vdesMapAddPoint(next); + }; + function renderVdesHistory() { + pruneVdesMessageHistory(); + if (!vdesMessagesEl) { + updateVdesSummary(); + return; + } + const fragment = document.createDocumentFragment(); + for (const message of vdesMessageHistory) { + fragment.appendChild(renderVdesRow(message)); + } + vdesMessagesEl.replaceChildren(fragment); + updateVdesSummary(); } -}; -window.pruneVdesHistoryView = function() { - pruneVdesMessageHistory(); - updateVdesBar(); - renderVdesHistory(); -}; -updateVdesSummary(); -if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("vdes"); + function addVdesMessage(msg) { + const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); + msg._tsMs = tsMs; + msg._ts = new Date(tsMs).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); + vdesMessageHistory.unshift(msg); + pruneVdesMessageHistory(); + scheduleVdesBarUpdate(); + scheduleVdesHistoryRender(); + } + function normalizeServerVdesMessage(msg) { + return { + ...msg, + rig_id: msg.rig_id || null + }; + } + vdesWindow.onServerVdesBatch = function(messages) { + if (!Array.isArray(messages) || messages.length === 0) return; + if (vdesStatus) vdesStatus.textContent = "Receiving"; + const normalized = []; + for (const msg of messages) { + const next = normalizeServerVdesMessage(msg); + const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now(); + next._tsMs = tsMs; + next._ts = new Date(tsMs).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); + if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) { + vdesWindow.vdesMapAddPoint(next); + } + normalized.push(next); + } + normalized.reverse(); + vdesMessageHistory = normalized.concat(vdesMessageHistory); + pruneVdesMessageHistory(); + scheduleVdesBarUpdate(); + scheduleVdesHistoryRender(); + }; + vdesWindow.restoreVdesHistory = function(messages) { + vdesWindow.onServerVdesBatch?.(messages); + }; + document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => { + void (async () => { + if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return; + try { + await vdesWindow.postPath?.("/clear_vdes_decode"); + vdesWindow.resetVdesHistoryView?.(); + } catch (e) { + console.error("VDES history clear failed", e); + } + })(); + }); + if (vdesFilterInput) { + vdesFilterInput.addEventListener("input", () => { + vdesFilterText = vdesFilterInput.value.trim().toUpperCase(); + renderVdesHistory(); + }); + } + vdesWindow.onServerVdes = function(msg) { + if (vdesStatus) vdesStatus.textContent = "Receiving"; + const next = normalizeServerVdesMessage(msg); + addVdesMessage(next); + if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) { + vdesWindow.vdesMapAddPoint(next); + } + }; + vdesWindow.pruneVdesHistoryView = function() { + pruneVdesMessageHistory(); + updateVdesBar(); + renderVdesHistory(); + }; + updateVdesSummary(); + vdesWindow._trxDrainPendingDecode?.("vdes"); +})(); 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 53e313bd..6ce24053 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 @@ -31,7 +31,6 @@ await build({ "sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"), scheduler: path.join(sourceDir, "plugins", "scheduler.js"), vchan: path.join(sourceDir, "plugins", "vchan.js"), - vdes: path.join(sourceDir, "plugins", "vdes.js"), wefax: path.join(sourceDir, "plugins", "wefax.js"), }, outdir: outputDir, @@ -51,6 +50,7 @@ await build({ cw: path.join(sourceDir, "plugins", "cw.ts"), ft8: path.join(sourceDir, "plugins", "ft8.ts"), "leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"), + vdes: path.join(sourceDir, "plugins", "vdes.ts"), }, outdir: outputDir, bundle: true, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts index cdf6c769..2cc0a37a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts @@ -16,7 +16,7 @@ const pluginGroups: Readonly> = { const loaded = new Set(); const loading = new Map>(); -const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js"]); +const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]); function loadLegacyScript(path: string): Promise { return new Promise((resolve, reject) => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts similarity index 56% rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.js rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts index eac37c1c..25cbd75f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts @@ -2,54 +2,107 @@ // // SPDX-License-Identifier: GPL-2.0-or-later +export {}; + +interface VdesMessage { + rig_id?: string | null; + message_type?: number; + bit_len?: number; + raw_bytes?: number[]; + lat?: number | null; + lon?: number | null; + vessel_name?: string | null; + callsign?: string | null; + destination?: string | null; + message_label?: string | null; + session_id?: number | null; + source_id?: number | null; + destination_id?: number | null; + data_count?: number | null; + asm_identifier?: number | null; + ack_nack_mask?: number | null; + channel_quality?: number | null; + payload_preview?: string | null; + link_id?: number | null; + sync_score?: number | null; + sync_errors?: number | null; + phase_rotation?: number | null; + fec_state?: string | null; + ts_ms?: number | null; + _tsMs?: number; + _ts?: string; +} + +interface VdesBridge { + getDecodeHistoryRetentionMs?: () => number; + trxScheduleUiFrameJob?: (key: string, job: () => void) => void; + escapeMapHtml?: (input: string) => string; + postPath?: (path: string) => Promise; + trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise }; + vdesMapAddPoint?: (message: VdesMessage) => void; + updateVdesBar?: () => void; + clearVdesBar?: () => void; + resetVdesHistoryView?: () => void; + onServerVdesBatch?: (messages: VdesMessage[]) => void; + restoreVdesHistory?: (messages: VdesMessage[]) => void; + onServerVdes?: (message: VdesMessage) => void; + pruneVdesHistoryView?: () => void; + _trxDrainPendingDecode?: (decoder: string) => void; +} + +const vdesWindow = window as unknown as VdesBridge; +const escapeVdesHtml = (input: string): string => vdesWindow.escapeMapHtml?.(input) ?? input + .replaceAll("&", "&").replaceAll("<", "<") + .replaceAll(">", ">").replaceAll('"', """); + // --- VDES Decoder Plugin (server-side decode) --- const vdesStatus = document.getElementById("vdes-status"); const vdesMessagesEl = document.getElementById("vdes-messages"); -const vdesFilterInput = document.getElementById("vdes-filter"); +const vdesFilterInput = document.getElementById("vdes-filter") as HTMLInputElement | null; const vdesBarOverlay = document.getElementById("vdes-bar-overlay"); const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary"); const vdesFrameCountEl = document.getElementById("vdes-frame-count"); const vdesLatestSeenEl = document.getElementById("vdes-latest-seen"); const VDES_BAR_WINDOW_MS = 15 * 60 * 1000; let vdesFilterText = ""; -let vdesMessageHistory = []; +let vdesMessageHistory: VdesMessage[] = []; -function currentVdesHistoryRetentionMs() { - return typeof window.getDecodeHistoryRetentionMs === "function" - ? window.getDecodeHistoryRetentionMs() +function currentVdesHistoryRetentionMs(): number { + return typeof vdesWindow.getDecodeHistoryRetentionMs === "function" + ? vdesWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1000; } function pruneVdesMessageHistory() { const cutoffMs = Date.now() - currentVdesHistoryRetentionMs(); - vdesMessageHistory = vdesMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs); + vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs); } -function scheduleVdesUi(key, job) { - if (typeof window.trxScheduleUiFrameJob === "function") { - window.trxScheduleUiFrameJob(key, job); +function scheduleVdesUi(key: string, job: () => void): void { + if (typeof vdesWindow.trxScheduleUiFrameJob === "function") { + vdesWindow.trxScheduleUiFrameJob(key, job); return; } job(); } function scheduleVdesHistoryRender() { - scheduleVdesUi("vdes-history", () => renderVdesHistory()); + scheduleVdesUi("vdes-history", () => { renderVdesHistory(); }); } function scheduleVdesBarUpdate() { - scheduleVdesUi("vdes-bar", () => updateVdesBar()); + scheduleVdesUi("vdes-bar", () => { updateVdesBar(); }); } function currentVdesCenterText() { - const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, ""); + const raw = ((document.getElementById("freq") as HTMLInputElement | null)?.value || "").replace(/[^\d]/g, ""); const hz = raw ? Number(raw) : 0; if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency"; return `100 kHz @ ${(hz / 1_000_000).toFixed(3)} MHz`; } -function vdesAgeText(tsMs) { - if (!Number.isFinite(tsMs)) return "just now"; +function vdesAgeText(tsMs: number | undefined): string { + if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now"; const deltaMs = Math.max(0, Date.now() - tsMs); const seconds = Math.round(deltaMs / 1000); if (seconds < 5) return "just now"; @@ -60,11 +113,11 @@ function vdesAgeText(tsMs) { return `${hours}h ago`; } -function vdesHexPreview(rawBytes) { +function vdesHexPreview(rawBytes: number[] | undefined): string { if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--"; return rawBytes .slice(0, 20) - .map((value) => Number(value).toString(16).padStart(2, "0")) + .map((value) => value.toString(16).padStart(2, "0")) .join(" ") .toUpperCase(); } @@ -84,7 +137,7 @@ function updateVdesSummary() { } } -function applyVdesFilterToRow(row) { +function applyVdesFilterToRow(row: HTMLElement): void { if (!vdesFilterText) { row.style.display = ""; return; @@ -93,12 +146,7 @@ function applyVdesFilterToRow(row) { row.style.display = text.includes(vdesFilterText) ? "" : "none"; } -function applyVdesFilterToAll() { - if (!vdesMessagesEl) return; - vdesMessagesEl.querySelectorAll(".vdes-message").forEach((row) => applyVdesFilterToRow(row)); -} - -function renderVdesRow(msg) { +function renderVdesRow(msg: VdesMessage): HTMLElement { const row = document.createElement("div"); row.className = "vdes-message"; const ts = msg._ts || new Date().toLocaleTimeString([], { @@ -150,32 +198,32 @@ function renderVdesRow(msg) { row.innerHTML = `
` + `${ts}` + - `${escapeMapHtml(title)}` + - `${escapeMapHtml(label)}` + - (labelText ? `${escapeMapHtml(labelText)}` : "") + - (linkText ? `${escapeMapHtml(linkText)}` : "") + - (srcText ? `${escapeMapHtml(srcText)}` : "") + - (dstText ? `${escapeMapHtml(dstText)}` : "") + - (syncText ? `${escapeMapHtml(syncText)}` : "") + - (phaseText ? `${escapeMapHtml(phaseText)}` : "") + - `T${escapeMapHtml(String(msg.message_type ?? "--"))}` + + `${escapeVdesHtml(title)}` + + `${escapeVdesHtml(label)}` + + (labelText ? `${escapeVdesHtml(labelText)}` : "") + + (linkText ? `${escapeVdesHtml(linkText)}` : "") + + (srcText ? `${escapeVdesHtml(srcText)}` : "") + + (dstText ? `${escapeVdesHtml(dstText)}` : "") + + (syncText ? `${escapeVdesHtml(syncText)}` : "") + + (phaseText ? `${escapeVdesHtml(phaseText)}` : "") + + `T${escapeVdesHtml(String(msg.message_type ?? "--"))}` + `
` + `
` + - `${escapeMapHtml(currentVdesCenterText())}` + - `${escapeMapHtml(`${msg.bit_len || 0} bits`)}` + - (sessionText ? `${escapeMapHtml(sessionText)}` : "") + - (asmText ? `${escapeMapHtml(asmText)}` : "") + - (countText ? `${escapeMapHtml(countText)}` : "") + - (ackText ? `${escapeMapHtml(ackText)}` : "") + - (cqiText ? `${escapeMapHtml(cqiText)}` : "") + - (info ? `${escapeMapHtml(info)}` : "") + - (fecText ? `${escapeMapHtml(fecText)}` : "") + - `${escapeMapHtml(vdesAgeText(msg._tsMs))}` + + `${escapeVdesHtml(currentVdesCenterText())}` + + `${escapeVdesHtml(`${msg.bit_len || 0} bits`)}` + + (sessionText ? `${escapeVdesHtml(sessionText)}` : "") + + (asmText ? `${escapeVdesHtml(asmText)}` : "") + + (countText ? `${escapeVdesHtml(countText)}` : "") + + (ackText ? `${escapeVdesHtml(ackText)}` : "") + + (cqiText ? `${escapeVdesHtml(cqiText)}` : "") + + (info ? `${escapeVdesHtml(info)}` : "") + + (fecText ? `${escapeVdesHtml(fecText)}` : "") + + `${escapeVdesHtml(vdesAgeText(msg._tsMs))}` + `
` + `
` + - (previewText ? `${escapeMapHtml(previewText)}` : "") + + (previewText ? `${escapeVdesHtml(previewText)}` : "") + (previewText ? `·` : "") + - `${escapeMapHtml(rawHex)}` + + `${escapeVdesHtml(rawHex)}` + `
`; applyVdesFilterToRow(row); return row; @@ -184,9 +232,9 @@ function renderVdesRow(msg) { function updateVdesBar() { if (!vdesBarOverlay) return; updateVdesSummary(); - const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES"; + const isVdes = ((document.getElementById("mode") as HTMLSelectElement | null)?.value || "").toUpperCase() === "VDES"; const cutoffMs = Date.now() - VDES_BAR_WINDOW_MS; - const messages = vdesMessageHistory.filter((msg) => msg._tsMs >= cutoffMs).slice(0, 6); + const messages = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs).slice(0, 6); if (!isVdes || messages.length === 0) { vdesBarOverlay.style.display = "none"; vdesBarOverlay.innerHTML = ""; @@ -196,19 +244,19 @@ function updateVdesBar() { let html = '
VDESLiveClearLast 15 minutes
'; for (const msg of messages) { const ts = msg._ts ? `${msg._ts}` : ""; - const label = escapeMapHtml(msg.callsign || "VDES"); - const title = escapeMapHtml(msg.vessel_name || "Burst"); + const label = escapeVdesHtml(msg.callsign || "VDES"); + const title = escapeVdesHtml(msg.vessel_name || "Burst"); const detail = [ `${msg.bit_len || 0} bits`, - msg.message_label ? escapeMapHtml(msg.message_label) : null, + msg.message_label ? escapeVdesHtml(msg.message_label) : null, Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null, Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null, Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null, Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null, Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null, Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null, - msg.destination ? escapeMapHtml(msg.destination) : null, - escapeMapHtml(vdesAgeText(msg._tsMs)), + msg.destination ? escapeVdesHtml(msg.destination) : null, + escapeVdesHtml(vdesAgeText(msg._tsMs)), ] .filter(Boolean) .join(" · "); @@ -217,12 +265,12 @@ function updateVdesBar() { vdesBarOverlay.innerHTML = html; vdesBarOverlay.style.display = "flex"; } -window.updateVdesBar = updateVdesBar; -window.clearVdesBar = function() { - window.resetVdesHistoryView(); +vdesWindow.updateVdesBar = updateVdesBar; +vdesWindow.clearVdesBar = function() { + vdesWindow.resetVdesHistoryView?.(); }; -window.resetVdesHistoryView = function() { +vdesWindow.resetVdesHistoryView = function() { if (vdesMessagesEl) vdesMessagesEl.innerHTML = ""; vdesMessageHistory = []; updateVdesBar(); @@ -236,14 +284,14 @@ function renderVdesHistory() { return; } const fragment = document.createDocumentFragment(); - for (let i = 0; i < vdesMessageHistory.length; i += 1) { - fragment.appendChild(renderVdesRow(vdesMessageHistory[i])); + for (const message of vdesMessageHistory) { + fragment.appendChild(renderVdesRow(message)); } vdesMessagesEl.replaceChildren(fragment); updateVdesSummary(); } -function addVdesMessage(msg) { +function addVdesMessage(msg: VdesMessage): void { const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); msg._tsMs = tsMs; msg._ts = new Date(tsMs).toLocaleTimeString([], { @@ -258,39 +306,17 @@ function addVdesMessage(msg) { scheduleVdesHistoryRender(); } -function normalizeServerVdesMessage(msg) { +function normalizeServerVdesMessage(msg: VdesMessage): VdesMessage { return { + ...msg, rig_id: msg.rig_id || null, - message_type: msg.message_type, - bit_len: msg.bit_len, - raw_bytes: msg.raw_bytes, - lat: msg.lat, - lon: msg.lon, - vessel_name: msg.vessel_name, - callsign: msg.callsign, - destination: msg.destination, - message_label: msg.message_label, - session_id: msg.session_id, - source_id: msg.source_id, - destination_id: msg.destination_id, - data_count: msg.data_count, - asm_identifier: msg.asm_identifier, - ack_nack_mask: msg.ack_nack_mask, - channel_quality: msg.channel_quality, - payload_preview: msg.payload_preview, - link_id: msg.link_id, - sync_score: msg.sync_score, - sync_errors: msg.sync_errors, - phase_rotation: msg.phase_rotation, - fec_state: msg.fec_state, - ts_ms: msg.ts_ms, }; } -window.onServerVdesBatch = function(messages) { +vdesWindow.onServerVdesBatch = function(messages: VdesMessage[]) { if (!Array.isArray(messages) || messages.length === 0) return; if (vdesStatus) vdesStatus.textContent = "Receiving"; - const normalized = []; + const normalized: VdesMessage[] = []; for (const msg of messages) { const next = normalizeServerVdesMessage(msg); const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now(); @@ -300,8 +326,8 @@ window.onServerVdesBatch = function(messages) { minute: "2-digit", second: "2-digit", }); - if (next.lat != null && next.lon != null && window.vdesMapAddPoint) { - window.vdesMapAddPoint(next); + if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) { + vdesWindow.vdesMapAddPoint(next); } normalized.push(next); } @@ -312,19 +338,19 @@ window.onServerVdesBatch = function(messages) { scheduleVdesHistoryRender(); }; -window.restoreVdesHistory = function(messages) { - window.onServerVdesBatch(messages); +vdesWindow.restoreVdesHistory = function(messages: VdesMessage[]) { + vdesWindow.onServerVdesBatch?.(messages); }; -document.getElementById("settings-clear-vdes-history")?.addEventListener("click", async () => { - if (!await window.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return; +document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => { void (async () => { + if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return; try { - await postPath("/clear_vdes_decode"); - window.resetVdesHistoryView(); + await vdesWindow.postPath?.("/clear_vdes_decode"); + vdesWindow.resetVdesHistoryView?.(); } catch (e) { console.error("VDES history clear failed", e); } -}); +})(); }); if (vdesFilterInput) { vdesFilterInput.addEventListener("input", () => { @@ -333,20 +359,20 @@ if (vdesFilterInput) { }); } -window.onServerVdes = function(msg) { +vdesWindow.onServerVdes = function(msg: VdesMessage) { if (vdesStatus) vdesStatus.textContent = "Receiving"; const next = normalizeServerVdesMessage(msg); addVdesMessage(next); - if (next.lat != null && next.lon != null && window.vdesMapAddPoint) { - window.vdesMapAddPoint(next); + if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) { + vdesWindow.vdesMapAddPoint(next); } }; -window.pruneVdesHistoryView = function() { +vdesWindow.pruneVdesHistoryView = function() { pruneVdesMessageHistory(); updateVdesBar(); renderVdesHistory(); }; updateVdesSummary(); -if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("vdes"); +vdesWindow._trxDrainPendingDecode?.("vdes"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/vdes.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/vdes.test.mjs new file mode 100644 index 00000000..35d30769 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/vdes.test.mjs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import vm from "node:vm"; + +test("VDES entry normalizes and forwards positioned server messages", async () => { + const forwarded = []; + const window = { + trxUi: { confirm: async () => true }, + vdesMapAddPoint: (message) => { forwarded.push(message); }, + }; + const context = vm.createContext({ + window, + document: { getElementById: () => null }, + Date, + Number, + String, + Array, + console, + }); + const source = await readFile(new URL("../../assets/web/generated/vdes.js", import.meta.url), "utf8"); + new vm.Script(source).runInContext(context); + + window.onServerVdes({ callsign: "SP0ABC", lat: 54.5, lon: 18.5, bit_len: 120, ts_ms: Date.now() }); + assert.equal(forwarded.length, 1); + assert.equal(forwarded[0].callsign, "SP0ABC"); + assert.equal(forwarded[0].rig_id, null); + assert.equal(typeof forwarded[0]._tsMs, "number"); +});