import { forActiveRig, isActiveRigDecode } from "./chunk-S57W63QN.js"; import { hostCore } from "./chunk-KL66PICH.js"; // src/plugins/vdes.ts var vdesWindow = window; var escapeVdesHtml = (input) => hostCore.escapeMapHtml(input); 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; } function pruneVdesMessageHistory() { const cutoffMs = Date.now() - currentVdesHistoryRetentionMs(); vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs); } function scheduleVdesUi(key, job) { if (typeof vdesWindow.trxScheduleUiFrameJob === "function") { vdesWindow.trxScheduleUiFrameJob(key, job); return; } 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 (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`; } 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(); } function vdesRigMessages() { return forActiveRig(vdesMessageHistory); } function updateVdesSummary() { pruneVdesMessageHistory(); if (vdesChannelSummaryEl) { vdesChannelSummaryEl.textContent = currentVdesCenterText(); } const rigMessages = vdesRigMessages(); if (vdesFrameCountEl) { const count = rigMessages.length; vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`; } if (vdesLatestSeenEl) { const latest = rigMessages[0]; vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet"; } } 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" }); 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 && isActiveRigDecode(msg.rig_id)).slice(0, 6); if (!isVdes || messages.length === 0) { vdesBarOverlay.style.display = "none"; vdesBarOverlay.innerHTML = ""; return; } 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"; } vdesWindow.updateVdesBar = updateVdesBar; vdesWindow.clearVdesBar = function() { resetVdesHistoryView(); }; function resetVdesHistoryView() { if (vdesMessagesEl) vdesMessagesEl.innerHTML = ""; vdesMessageHistory = []; updateVdesBar(); renderVdesHistory(); } function renderVdesHistory() { pruneVdesMessageHistory(); if (!vdesMessagesEl) { updateVdesSummary(); return; } const fragment = document.createDocumentFragment(); for (const message of vdesRigMessages()) { fragment.appendChild(renderVdesRow(message)); } 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 { ...msg, rig_id: msg.rig_id || null }; } function onServerVdesBatch(messages) { if (!Array.isArray(messages) || messages.length === 0) return; const normalized = []; for (const msg of messages) { const next = normalizeServerVdesMessage(msg); if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving"; 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" }); plotVdesMessage(next); normalized.push(next); } normalized.reverse(); vdesMessageHistory = normalized.concat(vdesMessageHistory); pruneVdesMessageHistory(); scheduleVdesBarUpdate(); scheduleVdesHistoryRender(); } 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 hostCore.postPath("/clear_vdes_decode"); resetVdesHistoryView(); } catch (e) { console.error("VDES history clear failed", e); } })(); }); if (vdesFilterInput) { vdesFilterInput.addEventListener("input", () => { vdesFilterText = vdesFilterInput.value.trim().toUpperCase(); renderVdesHistory(); }); } function plotVdesMessage(msg) { if (msg.lat == null || msg.lon == null || !vdesWindow.vdesMapAddPoint) return; vdesWindow.vdesMapAddPoint(msg); } function onServerVdes(msg) { const next = normalizeServerVdesMessage(msg); if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving"; addVdesMessage(next); plotVdesMessage(next); } function pruneVdesHistoryView() { pruneVdesMessageHistory(); updateVdesBar(); renderVdesHistory(); } updateVdesSummary(); window.trxPluginRuntime.registerDecoder({ id: "vdes", onMessage: onServerVdes, onBatch: onServerVdesBatch, restore: onServerVdesBatch, reset: resetVdesHistoryView, prune: pruneVdesHistoryView, rerender: () => { updateVdesBar(); renderVdesHistory(); }, // Oldest first, so tracks are rebuilt in the order they happened. syncMap: () => { for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry); } });