// --- 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 vdesPauseBtn = document.getElementById("vdes-pause-btn"); const vdesClearBtn = document.getElementById("vdes-clear-btn"); 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_MAX_MESSAGES = 200; const VDES_BAR_WINDOW_MS = 15 * 60 * 1000; let vdesFilterText = ""; let vdesMessageHistory = []; let vdesPaused = false; let vdesBufferedWhilePaused = 0; 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 / 1_000_000).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 / 1000); 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() { if (vdesChannelSummaryEl) { vdesChannelSummaryEl.textContent = currentVdesCenterText(); } if (vdesFrameCountEl) { const count = vdesMessageHistory.length; let text = `${count} burst${count === 1 ? "" : "s"}`; if (vdesPaused && vdesBufferedWhilePaused > 0) { text += ` · ${vdesBufferedWhilePaused} buffered`; } vdesFrameCountEl.textContent = text; } if (vdesLatestSeenEl) { const latest = vdesMessageHistory[0]; vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet"; } if (vdesPauseBtn) { vdesPauseBtn.textContent = vdesPaused ? "Resume" : "Pause"; vdesPauseBtn.classList.toggle("active", vdesPaused); } } function applyVdesFilterToRow(row) { if (!vdesFilterText) { row.style.display = ""; return; } 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 || 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; } 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}
`; } vdesBarOverlay.innerHTML = html; vdesBarOverlay.style.display = "flex"; } window.updateVdesBar = updateVdesBar; window.clearVdesBar = function() { document.getElementById("vdes-clear-btn")?.click(); }; window.resetVdesHistoryView = function() { if (vdesMessagesEl) vdesMessagesEl.innerHTML = ""; vdesMessageHistory = []; vdesBufferedWhilePaused = 0; updateVdesBar(); renderVdesHistory(); }; function renderVdesHistory() { if (!vdesMessagesEl || vdesPaused) { updateVdesSummary(); return; } vdesMessagesEl.innerHTML = ""; for (let i = 0; i < vdesMessageHistory.length; i += 1) { vdesMessagesEl.appendChild(renderVdesRow(vdesMessageHistory[i])); } 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); if (vdesMessageHistory.length > VDES_MAX_MESSAGES) vdesMessageHistory.length = VDES_MAX_MESSAGES; updateVdesBar(); if (vdesPaused) { vdesBufferedWhilePaused += 1; updateVdesSummary(); } else { renderVdesHistory(); } } if (vdesClearBtn) { vdesClearBtn.addEventListener("click", async () => { try { await postPath("/clear_vdes_decode"); window.resetVdesHistoryView(); } catch (e) { console.error("VDES clear failed", e); } }); } if (vdesPauseBtn) { vdesPauseBtn.addEventListener("click", () => { vdesPaused = !vdesPaused; if (!vdesPaused) { vdesBufferedWhilePaused = 0; renderVdesHistory(); } else { updateVdesSummary(); } }); } if (vdesFilterInput) { vdesFilterInput.addEventListener("input", () => { vdesFilterText = vdesFilterInput.value.trim().toUpperCase(); renderVdesHistory(); }); } window.onServerVdes = function(msg) { if (vdesStatus) vdesStatus.textContent = vdesPaused ? "Paused" : "Receiving"; addVdesMessage({ 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, }); if (msg.lat != null && msg.lon != null && window.vdesMapAddPoint) { window.vdesMapAddPoint(msg); } }; updateVdesSummary();