// --- HF APRS Decoder Plugin (server-side decode, 300 baud) --- const hfAprsStatus = document.getElementById("hf-aprs-status"); const hfAprsPacketsEl = document.getElementById("hf-aprs-packets"); const hfAprsFilterInput = document.getElementById("hf-aprs-filter"); const hfAprsOnlyPosBtn = document.getElementById("hf-aprs-only-pos-btn"); const hfAprsHideCrcBtn = document.getElementById("hf-aprs-hide-crc-btn"); const hfAprsCollapseDupBtn = document.getElementById("hf-aprs-collapse-dup-btn"); const hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count"); const hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count"); const hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen"); let hfAprsFilterText = ""; let hfAprsPacketHistory = []; let hfAprsOnlyPos = false; let hfAprsHideCrc = false; let hfAprsCollapseDup = false; let hfAprsTypeFilter = "all"; function currentHfAprsHistoryRetentionMs() { return typeof window.getDecodeHistoryRetentionMs === "function" ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1000; } function pruneHfAprsPacketHistory() { const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs(); hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => Number(pkt?._tsMs) >= cutoffMs); } function scheduleHfAprsHistoryRender() { if (typeof window.trxScheduleUiFrameJob === "function") { window.trxScheduleUiFrameJob("hf-aprs-history", () => renderHfAprsHistory()); return; } renderHfAprsHistory(); } function hfAprsPacketCategory(pkt) { const type = String(pkt.type || "").toLowerCase(); const info = String(pkt.info || "").toLowerCase(); if (pkt.lat != null && pkt.lon != null || type.includes("position")) return "position"; if (type.includes("message") || info.startsWith(":")) return "message"; if (type.includes("weather") || info.startsWith("_")) return "weather"; if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry"; return "other"; } function hfAprsCategoryLabel(category) { switch (category) { case "position": return "Position"; case "message": return "Message"; case "weather": return "Weather"; case "telemetry": return "Telemetry"; default: return "Other"; } } function hfAprsAgeText(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 hfAprsDistanceText(pkt) { if (serverLat == null || serverLon == null || pkt.lat == null || pkt.lon == null) return ""; const distKm = haversineKm(serverLat, serverLon, pkt.lat, pkt.lon); if (!Number.isFinite(distKm)) return ""; if (distKm < 1) return `${Math.round(distKm * 1000)} m from TRX`; return `${distKm.toFixed(1)} km from TRX`; } function hfAprsPacketSignature(pkt) { return [ pkt.srcCall || "", pkt.destCall || "", pkt.path || "", pkt.info || "", pkt.type || "", pkt.lat != null ? pkt.lat.toFixed(4) : "", pkt.lon != null ? pkt.lon.toFixed(4) : "", ].join("|"); } function hfAprsHexBytes(bytes) { if (!Array.isArray(bytes) || bytes.length === 0) return "--"; return bytes.map((b) => Number(b).toString(16).toUpperCase().padStart(2, "0")).join(" "); } function hfAprsFilterMatch(pkt) { if (hfAprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false; if (hfAprsHideCrc && !pkt.crcOk) return false; if (hfAprsTypeFilter !== "all" && hfAprsPacketCategory(pkt) !== hfAprsTypeFilter) return false; if (!hfAprsFilterText) return true; const haystack = [ pkt.srcCall, pkt.destCall, pkt.path, pkt.info, pkt.type, pkt.lat != null ? pkt.lat.toFixed(4) : "", pkt.lon != null ? pkt.lon.toFixed(4) : "", hfAprsPacketCategory(pkt), ] .filter(Boolean) .join(" ") .toUpperCase(); return haystack.includes(hfAprsFilterText); } function hfAprsVisiblePackets() { const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory; return packets.filter(hfAprsFilterMatch); } function collapseHfAprsDuplicates(packets) { const seen = new Set(); const out = []; for (const pkt of packets) { const key = hfAprsPacketSignature(pkt); if (seen.has(key)) continue; seen.add(key); out.push(pkt); } return out; } function updateHfAprsSummary() { const visible = hfAprsVisiblePackets(); if (hfAprsTotalCountEl) { hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`; } if (hfAprsVisibleCountEl) { hfAprsVisibleCountEl.textContent = `${visible.length} shown`; } if (hfAprsLatestSeenEl) { const latest = hfAprsPacketHistory[0]; if (!latest) { hfAprsLatestSeenEl.textContent = "No packets yet"; } else { hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${hfAprsAgeText(latest._tsMs)}`; } } } function updateHfAprsChipState() { document.querySelectorAll("[id^='hf-aprs-type-']").forEach((btn) => { btn.classList.toggle("active", btn.id === `hf-aprs-type-${hfAprsTypeFilter}`); }); hfAprsOnlyPosBtn?.classList.toggle("active", hfAprsOnlyPos); hfAprsHideCrcBtn?.classList.toggle("active", hfAprsHideCrc); hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup); } function renderHfAprsInfo(pkt) { const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null; if (bytes && bytes.length > 0) { let out = ""; for (let i = 0; i < bytes.length; i++) { const b = bytes[i]; if (b >= 0x20 && b <= 0x7e) { const ch = String.fromCharCode(b); if (ch === "<") out += "<"; else if (ch === ">") out += ">"; else if (ch === "&") out += "&"; else if (ch === '"') out += """; else out += ch; } else { const hex = b.toString(16).toUpperCase().padStart(2, "0"); out += `0x${hex}`; } } return out; } const str = pkt.info || ""; let out = ""; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); if (code >= 0x20 && code <= 0x7e) { const ch = str[i]; if (ch === "<") out += "<"; else if (ch === ">") out += ">"; else if (ch === "&") out += "&"; else if (ch === '"') out += """; else out += ch; } else { const hex = code.toString(16).toUpperCase().padStart(2, "0"); out += `0x${hex}`; } } return out; } function renderHfAprsRow(pkt, isFresh) { const row = document.createElement("div"); row.className = "aprs-packet"; if (!pkt.crcOk) row.classList.add("aprs-packet-crc"); if (isFresh) row.classList.add("aprs-packet-new"); const ts = pkt._ts || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); const age = hfAprsAgeText(pkt._tsMs); const category = hfAprsPacketCategory(pkt); const categoryLabel = hfAprsCategoryLabel(category); const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`; const pathBadge = pkt.path ? `${escapeMapHtml(pkt.path)}` : ""; const crcBadge = pkt.crcOk ? "" : 'CRC Fail'; const hfBadge = 'HF'; let symbolHtml = ""; if (pkt.symbolTable && pkt.symbolCode) { const sheet = pkt.symbolTable === "/" ? 0 : 1; const code = pkt.symbolCode.charCodeAt(0) - 33; const col = code % 16; const row2 = Math.floor(code / 16); const bgX = -(col * 24); const bgY = -(row2 * 24); symbolHtml = ``; } const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : ""; const distance = hfAprsDistanceText(pkt); const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`; row.innerHTML = `