// --- 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 = '';
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();