diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js index 06e4b640..444d1036 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js @@ -1,5 +1,107 @@ "use strict"; (() => { + // src/plugins/aprs-shared.ts + function aprsPacketCategory(packet) { + const type = (packet.type ?? "").toLowerCase(); + const info = (packet.info ?? "").toLowerCase(); + if (packet.lat != null && packet.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 aprsCategoryLabel(category) { + switch (category) { + case "position": + return "Position"; + case "message": + return "Message"; + case "weather": + return "Weather"; + case "telemetry": + return "Telemetry"; + default: + return "Other"; + } + } + function aprsAgeText(timestampMs) { + if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now"; + const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${String(seconds)}s ago`; + const minutes = Math.round(seconds / 60); + if (minutes < 60) return `${String(minutes)}m ago`; + return `${String(Math.round(minutes / 60))}h ago`; + } + function aprsPacketSignature(packet) { + return [ + packet.srcCall ?? "", + packet.destCall ?? "", + packet.path ?? "", + packet.info ?? "", + packet.type ?? "", + packet.lat?.toFixed(4) ?? "", + packet.lon?.toFixed(4) ?? "" + ].join("|"); + } + function collapseAprsDuplicates(packets) { + const seen = /* @__PURE__ */ new Set(); + return packets.filter((packet) => { + const signature = aprsPacketSignature(packet); + if (seen.has(signature)) return false; + seen.add(signature); + return true; + }); + } + function aprsHexBytes(bytes) { + if (!bytes?.length) return "--"; + return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" "); + } + function renderAprsInfo(packet) { + if (packet.info_bytes?.length) { + return packet.info_bytes.map((byte) => renderAprsByte(byte)).join(""); + } + return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join(""); + } + function renderAprsByte(byte) { + return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`; + } + function renderAprsCharacter(character) { + const code = character.charCodeAt(0); + return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`; + } + function escapeAprsCharacter(character) { + if (character === "<") return "<"; + if (character === ">") return ">"; + if (character === "&") return "&"; + if (character === '"') return """; + return character; + } + function renderLocalAprsSymbol(packet, escapeHtml) { + if (!packet.symbolTable || !packet.symbolCode) return ""; + const symbol = escapeHtml(packet.symbolCode); + const table = packet.symbolTable === "/" ? "Primary" : "Alternate"; + return `${symbol}`; + } + function normalizeAprsPacket(packet, receiver) { + return { + rig_id: packet.rig_id || null, + receiver, + srcCall: packet.src_call ?? "", + destCall: packet.dest_call ?? "", + path: packet.path ?? "", + info: packet.info ?? "", + info_bytes: packet.info_bytes ?? [], + type: packet.packet_type ?? "", + crcOk: packet.crc_ok ?? false, + ts_ms: packet.ts_ms ?? null, + lat: packet.lat ?? null, + lon: packet.lon ?? null, + symbolTable: packet.symbol_table ?? null, + symbolCode: packet.symbol_code ?? null + }; + } + // src/plugins/aprs.ts var aprsWindow = window; var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); @@ -48,77 +150,6 @@ updateAprsBar(); }); } - function renderAprsInfo(pkt) { - const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null; - if (bytes && bytes.length > 0) { - let out2 = ""; - for (const b of bytes) { - if (b >= 32 && b <= 126) { - const ch = String.fromCharCode(b); - if (ch === "<") out2 += "<"; - else if (ch === ">") out2 += ">"; - else if (ch === "&") out2 += "&"; - else if (ch === '"') out2 += """; - else out2 += ch; - } else { - const hex = b.toString(16).toUpperCase().padStart(2, "0"); - out2 += `0x${hex}`; - } - } - return out2; - } - const str = pkt.info || ""; - let out = ""; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - if (code >= 32 && code <= 126) { - const ch = str.charAt(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 aprsPacketCategory(pkt) { - const type = (pkt.type ?? "").toLowerCase(); - const info = (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 aprsCategoryLabel(category) { - switch (category) { - case "position": - return "Position"; - case "message": - return "Message"; - case "weather": - return "Weather"; - case "telemetry": - return "Telemetry"; - default: - return "Other"; - } - } - function aprsAgeText(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 aprsDistanceText(pkt) { if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return ""; const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.lon); @@ -126,21 +157,6 @@ if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`; return `${distKm.toFixed(1)} km from TRX`; } - function aprsPacketSignature(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 aprsHexBytes(bytes) { - if (!Array.isArray(bytes) || bytes.length === 0) return "--"; - return bytes.map((b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" "); - } function aprsFilterMatch(pkt) { if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false; if (aprsHideCrc && !pkt.crcOk) return false; @@ -162,17 +178,6 @@ const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory; return packets.filter(aprsFilterMatch); } - function collapseAprsDuplicates(packets) { - const seen = /* @__PURE__ */ new Set(); - const out = []; - for (const pkt of packets) { - const key = aprsPacketSignature(pkt); - if (seen.has(key)) continue; - seen.add(key); - out.push(pkt); - } - return out; - } function updateAprsSummary() { const visible = aprsVisiblePackets(); if (aprsTotalCountEl) { @@ -210,12 +215,7 @@ const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`; const pathBadge = pkt.path ? `${escapeAprsHtml(pkt.path)}` : ""; const crcBadge = pkt.crcOk ? "" : 'CRC Fail'; - let symbolHtml = ""; - if (pkt.symbolTable && pkt.symbolCode) { - const symbol = escapeAprsHtml(pkt.symbolCode); - const table = pkt.symbolTable === "/" ? "Primary" : "Alternate"; - symbolHtml = `${symbol}`; - } + const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml); const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : ""; const distance = aprsDistanceText(pkt); const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`; @@ -325,22 +325,7 @@ scheduleAprsHistoryRender(); } function normalizeServerAprsPacket(pkt) { - return { - rig_id: pkt.rig_id || null, - receiver: aprsWindow.getDecodeRigMeta?.() ?? null, - srcCall: pkt.src_call ?? "", - destCall: pkt.dest_call ?? "", - path: pkt.path ?? "", - info: pkt.info ?? "", - info_bytes: pkt.info_bytes ?? [], - type: pkt.packet_type ?? "", - crcOk: pkt.crc_ok ?? false, - ts_ms: pkt.ts_ms ?? null, - lat: pkt.lat ?? null, - lon: pkt.lon ?? null, - symbolTable: pkt.symbol_table ?? null, - symbolCode: pkt.symbol_code ?? null - }; + return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null); } aprsWindow.onServerAprsBatch = function(packets) { if (!Array.isArray(packets) || packets.length === 0) return; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js index 22f7a956..90def677 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js @@ -1,360 +1,355 @@ "use strict"; -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 * 1e3; -} -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; +(() => { + // src/plugins/aprs-shared.ts + function aprsPacketCategory(packet) { + const type = (packet.type ?? "").toLowerCase(); + const info = (packet.info ?? "").toLowerCase(); + if (packet.lat != null && packet.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"; } - 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 / 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 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 * 1e3)} 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 = /* @__PURE__ */ 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 aprsCategoryLabel(category) { + switch (category) { + case "position": + return "Position"; + case "message": + return "Message"; + case "weather": + return "Weather"; + case "telemetry": + return "Telemetry"; + default: + return "Other"; } } -} -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 out2 = ""; - for (let i = 0; i < bytes.length; i++) { - const b = bytes[i]; - if (b >= 32 && b <= 126) { - const ch = String.fromCharCode(b); - if (ch === "<") out2 += "<"; - else if (ch === ">") out2 += ">"; - else if (ch === "&") out2 += "&"; - else if (ch === '"') out2 += """; - else out2 += ch; + function aprsAgeText(timestampMs) { + if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now"; + const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${String(seconds)}s ago`; + const minutes = Math.round(seconds / 60); + if (minutes < 60) return `${String(minutes)}m ago`; + return `${String(Math.round(minutes / 60))}h ago`; + } + function aprsPacketSignature(packet) { + return [ + packet.srcCall ?? "", + packet.destCall ?? "", + packet.path ?? "", + packet.info ?? "", + packet.type ?? "", + packet.lat?.toFixed(4) ?? "", + packet.lon?.toFixed(4) ?? "" + ].join("|"); + } + function collapseAprsDuplicates(packets) { + const seen = /* @__PURE__ */ new Set(); + return packets.filter((packet) => { + const signature = aprsPacketSignature(packet); + if (seen.has(signature)) return false; + seen.add(signature); + return true; + }); + } + function aprsHexBytes(bytes) { + if (!bytes?.length) return "--"; + return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" "); + } + function renderAprsInfo(packet) { + if (packet.info_bytes?.length) { + return packet.info_bytes.map((byte) => renderAprsByte(byte)).join(""); + } + return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join(""); + } + function renderAprsByte(byte) { + return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`; + } + function renderAprsCharacter(character) { + const code = character.charCodeAt(0); + return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`; + } + function escapeAprsCharacter(character) { + if (character === "<") return "<"; + if (character === ">") return ">"; + if (character === "&") return "&"; + if (character === '"') return """; + return character; + } + function renderLocalAprsSymbol(packet, escapeHtml) { + if (!packet.symbolTable || !packet.symbolCode) return ""; + const symbol = escapeHtml(packet.symbolCode); + const table = packet.symbolTable === "/" ? "Primary" : "Alternate"; + return `${symbol}`; + } + function normalizeAprsPacket(packet, receiver) { + return { + rig_id: packet.rig_id || null, + receiver, + srcCall: packet.src_call ?? "", + destCall: packet.dest_call ?? "", + path: packet.path ?? "", + info: packet.info ?? "", + info_bytes: packet.info_bytes ?? [], + type: packet.packet_type ?? "", + crcOk: packet.crc_ok ?? false, + ts_ms: packet.ts_ms ?? null, + lat: packet.lat ?? null, + lon: packet.lon ?? null, + symbolTable: packet.symbol_table ?? null, + symbolCode: packet.symbol_code ?? null + }; + } + + // src/plugins/hf-aprs.ts + var hfAprsWindow = window; + var escapeHfAprsHtml = (input) => hfAprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + var hfAprsStatus = document.getElementById("hf-aprs-status"); + var hfAprsPacketsEl = document.getElementById("hf-aprs-packets"); + var hfAprsFilterInput = document.getElementById("hf-aprs-filter"); + var hfAprsOnlyPosBtn = document.getElementById("hf-aprs-only-pos-btn"); + var hfAprsHideCrcBtn = document.getElementById("hf-aprs-hide-crc-btn"); + var hfAprsCollapseDupBtn = document.getElementById("hf-aprs-collapse-dup-btn"); + var hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count"); + var hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count"); + var hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen"); + var hfAprsFilterText = ""; + var hfAprsPacketHistory = []; + var hfAprsOnlyPos = false; + var hfAprsHideCrc = false; + var hfAprsCollapseDup = false; + var hfAprsTypeFilter = "all"; + function currentHfAprsHistoryRetentionMs() { + return typeof hfAprsWindow.getDecodeHistoryRetentionMs === "function" ? hfAprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; + } + function pruneHfAprsPacketHistory() { + const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs(); + hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs); + } + function scheduleHfAprsHistoryRender() { + if (typeof hfAprsWindow.trxScheduleUiFrameJob === "function") { + hfAprsWindow.trxScheduleUiFrameJob("hf-aprs-history", () => { + renderHfAprsHistory(); + }); + return; + } + renderHfAprsHistory(); + } + function hfAprsDistanceText(pkt) { + if (hfAprsWindow.serverLat == null || hfAprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !hfAprsWindow.haversineKm) return ""; + const distKm = hfAprsWindow.haversineKm(hfAprsWindow.serverLat, hfAprsWindow.serverLon, pkt.lat, pkt.lon); + if (!Number.isFinite(distKm)) return ""; + if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`; + return `${distKm.toFixed(1)} km from TRX`; + } + function hfAprsFilterMatch(pkt) { + if (hfAprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false; + if (hfAprsHideCrc && !pkt.crcOk) return false; + if (hfAprsTypeFilter !== "all" && aprsPacketCategory(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) : "", + aprsPacketCategory(pkt) + ].filter(Boolean).join(" ").toUpperCase(); + return haystack.includes(hfAprsFilterText); + } + function hfAprsVisiblePackets() { + const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory; + return packets.filter(hfAprsFilterMatch); + } + var collapseHfAprsDuplicates = collapseAprsDuplicates; + 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 { - const hex = b.toString(16).toUpperCase().padStart(2, "0"); - out2 += `0x${hex}`; + hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`; } } - return out2; } - const str = pkt.info || ""; - let out = ""; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - if (code >= 32 && code <= 126) { - 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 || (/* @__PURE__ */ 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 = `
${ts}` + hfBadge + symbolHtml + `${escapeMapHtml(pkt.srcCall)}>${escapeMapHtml(pkt.destCall || "")}${escapeMapHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeMapHtml(age)}` + (distance ? `${escapeMapHtml(distance)}` : "") + `${escapeMapHtml(pkt.type || "--")}
${renderHfAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `` : "") + (pkt.lat != null && pkt.lon != null ? `` : "") + `QRZ
Details
Source${escapeMapHtml(pkt.srcCall || "--")}Destination${escapeMapHtml(pkt.destCall || "--")}Type${escapeMapHtml(pkt.type || "--")}Path${escapeMapHtml(pkt.path || "--")}Age${escapeMapHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeMapHtml(pkt.info || "--")}Info Bytes${escapeMapHtml(hfAprsHexBytes(pkt.info_bytes))}
`; - row.querySelectorAll("[data-aprs-map]").forEach((el) => { - el.addEventListener("click", (evt) => { - evt.preventDefault(); - const raw = String(el.dataset.aprsMap || ""); - const [lat, lon] = raw.split(",").map(Number); - if (window.navigateToAprsMap && Number.isFinite(lat) && Number.isFinite(lon)) { - window.navigateToAprsMap(lat, lon); - } + function updateHfAprsChipState() { + document.querySelectorAll("[id^='hf-aprs-type-']").forEach((btn) => { + btn.classList.toggle("active", btn.id === `hf-aprs-type-${hfAprsTypeFilter}`); }); - }); - const copyBtn = row.querySelector("[data-aprs-copy]"); - if (copyBtn) { - copyBtn.addEventListener("click", async () => { - const raw = String(copyBtn.dataset.aprsCopy || ""); - try { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(raw); - showHint("Coordinates copied", 1200); + hfAprsOnlyPosBtn?.classList.toggle("active", hfAprsOnlyPos); + hfAprsHideCrcBtn?.classList.toggle("active", hfAprsHideCrc); + hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup); + } + 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 || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + const age = aprsAgeText(pkt._tsMs); + const category = aprsPacketCategory(pkt); + const categoryLabel = aprsCategoryLabel(category); + const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`; + const pathBadge = pkt.path ? `${escapeHfAprsHtml(pkt.path)}` : ""; + const crcBadge = pkt.crcOk ? "" : 'CRC Fail'; + const hfBadge = 'HF'; + const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml); + 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 = `
${ts}` + hfBadge + symbolHtml + `${escapeHfAprsHtml(pkt.srcCall ?? "")}>${escapeHfAprsHtml(pkt.destCall || "")}${escapeHfAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeHfAprsHtml(age)}` + (distance ? `${escapeHfAprsHtml(distance)}` : "") + `${escapeHfAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `` : "") + (pkt.lat != null && pkt.lon != null ? `` : "") + `QRZ
Details
Source${escapeHfAprsHtml(pkt.srcCall || "--")}Destination${escapeHfAprsHtml(pkt.destCall || "--")}Type${escapeHfAprsHtml(pkt.type || "--")}Path${escapeHfAprsHtml(pkt.path || "--")}Age${escapeHfAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeHfAprsHtml(pkt.info || "--")}Info Bytes${escapeHfAprsHtml(aprsHexBytes(pkt.info_bytes))}
`; + row.querySelectorAll("[data-aprs-map]").forEach((el) => { + el.addEventListener("click", (evt) => { + evt.preventDefault(); + const raw = el.dataset.aprsMap ?? ""; + const [lat, lon] = raw.split(",").map(Number); + if (hfAprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) { + hfAprsWindow.navigateToAprsMap(lat, lon); } - } catch (_e) { - showHint("Copy failed", 1500); - } + }); }); + const copyBtn = row.querySelector("[data-aprs-copy]"); + if (copyBtn) { + copyBtn.addEventListener("click", () => { + void (async () => { + const raw = copyBtn.dataset.aprsCopy ?? ""; + try { + const clipboard = Reflect.get(navigator, "clipboard"); + if (clipboard) { + await clipboard.writeText(raw); + hfAprsWindow.showHint?.("Coordinates copied", 1200); + } + } catch { + hfAprsWindow.showHint?.("Copy failed", 1500); + } + })(); + }); + } + return row; } - return row; -} -function renderHfAprsHistory() { - pruneHfAprsPacketHistory(); - if (!hfAprsPacketsEl) { + function renderHfAprsHistory() { + pruneHfAprsPacketHistory(); + if (!hfAprsPacketsEl) { + updateHfAprsSummary(); + updateHfAprsChipState(); + return; + } + const visible = hfAprsVisiblePackets(); + const fragment = document.createDocumentFragment(); + for (const [index, packet] of visible.entries()) { + fragment.appendChild(renderHfAprsRow(packet, index === 0)); + } + hfAprsPacketsEl.replaceChildren(fragment); updateHfAprsSummary(); updateHfAprsChipState(); - return; } - const visible = hfAprsVisiblePackets(); - const fragment = document.createDocumentFragment(); - for (let i = 0; i < visible.length; i++) { - fragment.appendChild(renderHfAprsRow(visible[i], i === 0)); - } - hfAprsPacketsEl.replaceChildren(fragment); - updateHfAprsSummary(); - updateHfAprsChipState(); -} -window.resetHfAprsHistoryView = function() { - if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = ""; - hfAprsPacketHistory = []; - renderHfAprsHistory(); -}; -window.pruneHfAprsHistoryView = function() { - pruneHfAprsPacketHistory(); - renderHfAprsHistory(); -}; -function addHfAprsPacket(pkt) { - const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now(); - pkt._tsMs = tsMs; - pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); - hfAprsPacketHistory.unshift(pkt); - pruneHfAprsPacketHistory(); - scheduleHfAprsHistoryRender(); -} -function normalizeServerHfAprsPacket(pkt) { - return { - rig_id: pkt.rig_id || null, - receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null, - srcCall: pkt.src_call, - destCall: pkt.dest_call, - path: pkt.path, - info: pkt.info, - info_bytes: pkt.info_bytes, - type: pkt.packet_type, - crcOk: pkt.crc_ok, - ts_ms: pkt.ts_ms, - lat: pkt.lat, - lon: pkt.lon, - symbolTable: pkt.symbol_table, - symbolCode: pkt.symbol_code + hfAprsWindow.resetHfAprsHistoryView = function() { + if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = ""; + hfAprsPacketHistory = []; + renderHfAprsHistory(); }; -} -window.onServerHfAprsBatch = function(packets) { - if (!Array.isArray(packets) || packets.length === 0) return; - if (hfAprsStatus) hfAprsStatus.textContent = "Receiving"; - const normalized = []; - for (const pkt of packets) { - const next = normalizeServerHfAprsPacket(pkt); - 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" }); - normalized.push(next); + hfAprsWindow.pruneHfAprsHistoryView = function() { + pruneHfAprsPacketHistory(); + renderHfAprsHistory(); + }; + function addHfAprsPacket(pkt) { + const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now(); + pkt._tsMs = tsMs; + pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + hfAprsPacketHistory.unshift(pkt); + pruneHfAprsPacketHistory(); + scheduleHfAprsHistoryRender(); } - normalized.reverse(); - hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory); - pruneHfAprsPacketHistory(); - scheduleHfAprsHistoryRender(); -}; -window.restoreHfAprsHistory = function(packets) { - window.onServerHfAprsBatch(packets); -}; -const hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn"); -hfAprsDecodeToggleBtn?.addEventListener("click", async () => { - try { - await window.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn); - await postPath("/toggle_hf_aprs_decode"); - } catch (e) { - console.error("HF APRS toggle failed", e); + function normalizeServerHfAprsPacket(pkt) { + return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null); } -}); -document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", async () => { - if (!await window.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return; - try { - await postPath("/clear_hf_aprs_decode"); - window.resetHfAprsHistoryView(); - } catch (e) { - console.error("HF APRS history clear failed", e); + hfAprsWindow.onServerHfAprsBatch = function(packets) { + if (!Array.isArray(packets) || packets.length === 0) return; + if (hfAprsStatus) hfAprsStatus.textContent = "Receiving"; + const normalized = []; + for (const pkt of packets) { + const next = normalizeServerHfAprsPacket(pkt); + 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" }); + normalized.push(next); + } + normalized.reverse(); + hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory); + pruneHfAprsPacketHistory(); + scheduleHfAprsHistoryRender(); + }; + hfAprsWindow.restoreHfAprsHistory = function(packets) { + hfAprsWindow.onServerHfAprsBatch?.(packets); + }; + var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn"); + hfAprsDecodeToggleBtn?.addEventListener("click", () => { + void (async () => { + try { + await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn); + await hfAprsWindow.postPath?.("/toggle_hf_aprs_decode"); + } catch (e) { + console.error("HF APRS toggle failed", e); + } + })(); + }); + document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", () => { + void (async () => { + if (!await hfAprsWindow.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return; + try { + await hfAprsWindow.postPath?.("/clear_hf_aprs_decode"); + hfAprsWindow.resetHfAprsHistoryView?.(); + } catch (e) { + console.error("HF APRS history clear failed", e); + } + })(); + }); + if (hfAprsOnlyPosBtn) { + hfAprsOnlyPosBtn.addEventListener("click", () => { + hfAprsOnlyPos = !hfAprsOnlyPos; + renderHfAprsHistory(); + }); } -}); -if (hfAprsOnlyPosBtn) { - hfAprsOnlyPosBtn.addEventListener("click", () => { - hfAprsOnlyPos = !hfAprsOnlyPos; - renderHfAprsHistory(); + if (hfAprsHideCrcBtn) { + hfAprsHideCrcBtn.addEventListener("click", () => { + hfAprsHideCrc = !hfAprsHideCrc; + renderHfAprsHistory(); + }); + } + if (hfAprsCollapseDupBtn) { + hfAprsCollapseDupBtn.addEventListener("click", () => { + hfAprsCollapseDup = !hfAprsCollapseDup; + renderHfAprsHistory(); + }); + } + ["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => { + const btn = document.getElementById(`hf-aprs-type-${type}`); + if (!btn) return; + btn.addEventListener("click", () => { + hfAprsTypeFilter = type; + renderHfAprsHistory(); + }); }); -} -if (hfAprsHideCrcBtn) { - hfAprsHideCrcBtn.addEventListener("click", () => { - hfAprsHideCrc = !hfAprsHideCrc; - renderHfAprsHistory(); - }); -} -if (hfAprsCollapseDupBtn) { - hfAprsCollapseDupBtn.addEventListener("click", () => { - hfAprsCollapseDup = !hfAprsCollapseDup; - renderHfAprsHistory(); - }); -} -["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => { - const btn = document.getElementById(`hf-aprs-type-${type}`); - if (!btn) return; - btn.addEventListener("click", () => { - hfAprsTypeFilter = type; - renderHfAprsHistory(); - }); -}); -if (hfAprsFilterInput) { - hfAprsFilterInput.addEventListener("input", () => { - hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase(); - renderHfAprsHistory(); - }); -} -window.onServerHfAprs = function(pkt) { - if (hfAprsStatus) hfAprsStatus.textContent = "Receiving"; - addHfAprsPacket(normalizeServerHfAprsPacket(pkt)); -}; -renderHfAprsHistory(); -if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("hf_aprs"); + if (hfAprsFilterInput) { + hfAprsFilterInput.addEventListener("input", () => { + hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase(); + renderHfAprsHistory(); + }); + } + hfAprsWindow.onServerHfAprs = function(pkt) { + if (hfAprsStatus) hfAprsStatus.textContent = "Receiving"; + addHfAprsPacket(normalizeServerHfAprsPacket(pkt)); + }; + renderHfAprsHistory(); + hfAprsWindow._trxDrainPendingDecode?.("hf_aprs"); +})(); 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 9f5535aa..1607e28d 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", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js"]); +const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.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/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs index ecfdf169..81cd6014 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 @@ -23,7 +23,6 @@ await build({ screenshot: path.join(sourceDir, "screenshot.ts"), "webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"), bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"), - "hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.js"), sat: path.join(sourceDir, "plugins", "sat.js"), "sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"), scheduler: path.join(sourceDir, "plugins", "scheduler.js"), @@ -51,6 +50,7 @@ await build({ "background-decode": path.join(sourceDir, "plugins", "background-decode.ts"), ais: path.join(sourceDir, "plugins", "ais.ts"), aprs: path.join(sourceDir, "plugins", "aprs.ts"), + "hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.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 8b836047..fe93c2d6 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", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js"]); +const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.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/aprs-shared.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts new file mode 100644 index 00000000..df91abca --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +export type AprsCategory = "position" | "message" | "weather" | "telemetry" | "other"; +export type AprsTypeFilter = "all" | AprsCategory; + +export interface AprsPacket { + rig_id?: string | null; + receiver?: unknown; + srcCall?: string; + destCall?: string; + path?: string; + info?: string; + info_bytes?: number[]; + type?: string; + crcOk?: boolean; + ts_ms?: number | null; + lat?: number | null; + lon?: number | null; + symbolTable?: string | null; + symbolCode?: string | null; + _tsMs?: number; + _ts?: string; + src_call?: string; + dest_call?: string; + packet_type?: string; + crc_ok?: boolean; + symbol_table?: string | null; + symbol_code?: string | null; +} + +export function aprsPacketCategory(packet: AprsPacket): AprsCategory { + const type = (packet.type ?? "").toLowerCase(); + const info = (packet.info ?? "").toLowerCase(); + if ((packet.lat != null && packet.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"; +} + +export function aprsCategoryLabel(category: AprsCategory): string { + switch (category) { + case "position": return "Position"; + case "message": return "Message"; + case "weather": return "Weather"; + case "telemetry": return "Telemetry"; + default: return "Other"; + } +} + +export function aprsAgeText(timestampMs: number | undefined): string { + if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now"; + const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1000); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${String(seconds)}s ago`; + const minutes = Math.round(seconds / 60); + if (minutes < 60) return `${String(minutes)}m ago`; + return `${String(Math.round(minutes / 60))}h ago`; +} + +export function aprsPacketSignature(packet: AprsPacket): string { + return [packet.srcCall ?? "", packet.destCall ?? "", packet.path ?? "", packet.info ?? "", packet.type ?? "", + packet.lat?.toFixed(4) ?? "", packet.lon?.toFixed(4) ?? ""].join("|"); +} + +export function collapseAprsDuplicates(packets: AprsPacket[]): AprsPacket[] { + const seen = new Set(); + return packets.filter((packet) => { + const signature = aprsPacketSignature(packet); + if (seen.has(signature)) return false; + seen.add(signature); + return true; + }); +} + +export function aprsHexBytes(bytes: number[] | undefined): string { + if (!bytes?.length) return "--"; + return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" "); +} + +export function renderAprsInfo(packet: AprsPacket): string { + if (packet.info_bytes?.length) { + return packet.info_bytes.map((byte) => renderAprsByte(byte)).join(""); + } + return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join(""); +} + +function renderAprsByte(byte: number): string { + return byte >= 0x20 && byte <= 0x7e + ? escapeAprsCharacter(String.fromCharCode(byte)) + : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`; +} + +function renderAprsCharacter(character: string): string { + const code = character.charCodeAt(0); + return code >= 0x20 && code <= 0x7e + ? escapeAprsCharacter(character) + : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`; +} + +function escapeAprsCharacter(character: string): string { + if (character === "<") return "<"; + if (character === ">") return ">"; + if (character === "&") return "&"; + if (character === '"') return """; + return character; +} + +export function renderLocalAprsSymbol(packet: AprsPacket, escapeHtml: (value: string) => string): string { + if (!packet.symbolTable || !packet.symbolCode) return ""; + const symbol = escapeHtml(packet.symbolCode); + const table = packet.symbolTable === "/" ? "Primary" : "Alternate"; + return `${symbol}`; +} + +export function normalizeAprsPacket(packet: AprsPacket, receiver: unknown): AprsPacket { + return { + rig_id: packet.rig_id || null, + receiver, + srcCall: packet.src_call ?? "", + destCall: packet.dest_call ?? "", + path: packet.path ?? "", + info: packet.info ?? "", + info_bytes: packet.info_bytes ?? [], + type: packet.packet_type ?? "", + crcOk: packet.crc_ok ?? false, + ts_ms: packet.ts_ms ?? null, + lat: packet.lat ?? null, + lon: packet.lon ?? null, + symbolTable: packet.symbol_table ?? null, + symbolCode: packet.symbol_code ?? null, + }; +} diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts index 70dc6f38..e6e15a28 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts @@ -2,34 +2,18 @@ // // SPDX-License-Identifier: GPL-2.0-or-later -export {}; - -type AprsCategory = "position" | "message" | "weather" | "telemetry" | "other"; -type AprsTypeFilter = "all" | AprsCategory; -interface AprsPacket { - rig_id?: string | null; - receiver?: unknown; - srcCall?: string; - destCall?: string; - path?: string; - info?: string; - info_bytes?: number[]; - type?: string; - crcOk?: boolean; - ts_ms?: number | null; - lat?: number | null; - lon?: number | null; - symbolTable?: string | null; - symbolCode?: string | null; - _tsMs?: number; - _ts?: string; - src_call?: string; - dest_call?: string; - packet_type?: string; - crc_ok?: boolean; - symbol_table?: string | null; - symbol_code?: string | null; -} +import { + aprsAgeText, + aprsCategoryLabel, + aprsHexBytes, + aprsPacketCategory, + collapseAprsDuplicates, + normalizeAprsPacket, + renderAprsInfo, + renderLocalAprsSymbol, + type AprsPacket, + type AprsTypeFilter, +} from "./aprs-shared"; interface AprsBridge { getDecodeHistoryRetentionMs?: () => number; trxScheduleUiFrameJob?: (key: string, job: () => void) => void; @@ -107,76 +91,6 @@ function scheduleAprsBarUpdate() { scheduleAprsUi("aprs-bar", () => { updateAprsBar(); }); } -function renderAprsInfo(pkt: AprsPacket): string { - const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null; - if (bytes && bytes.length > 0) { - let out = ""; - for (const b of bytes) { - 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.charAt(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 aprsPacketCategory(pkt: AprsPacket): AprsCategory { - const type = (pkt.type ?? "").toLowerCase(); - const info = (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 aprsCategoryLabel(category: AprsCategory): string { - switch (category) { - case "position": return "Position"; - case "message": return "Message"; - case "weather": return "Weather"; - case "telemetry": return "Telemetry"; - default: return "Other"; - } -} - -function aprsAgeText(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"; - 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 aprsDistanceText(pkt: AprsPacket): string { if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return ""; const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.lon); @@ -185,23 +99,6 @@ function aprsDistanceText(pkt: AprsPacket): string { return `${distKm.toFixed(1)} km from TRX`; } -function aprsPacketSignature(pkt: AprsPacket): string { - 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 aprsHexBytes(bytes: number[] | undefined): string { - if (!Array.isArray(bytes) || bytes.length === 0) return "--"; - return bytes.map((b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" "); -} - function aprsFilterMatch(pkt: AprsPacket): boolean { if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false; if (aprsHideCrc && !pkt.crcOk) return false; @@ -228,18 +125,6 @@ function aprsVisiblePackets(): AprsPacket[] { return packets.filter(aprsFilterMatch); } -function collapseAprsDuplicates(packets: AprsPacket[]): AprsPacket[] { - const seen = new Set(); - const out: AprsPacket[] = []; - for (const pkt of packets) { - const key = aprsPacketSignature(pkt); - if (seen.has(key)) continue; - seen.add(key); - out.push(pkt); - } - return out; -} - function updateAprsSummary() { const visible = aprsVisiblePackets(); if (aprsTotalCountEl) { @@ -280,12 +165,7 @@ function renderAprsRow(pkt: AprsPacket, isFresh: boolean): HTMLElement { const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`; const pathBadge = pkt.path ? `${escapeAprsHtml(pkt.path)}` : ""; const crcBadge = pkt.crcOk ? "" : 'CRC Fail'; - let symbolHtml = ""; - if (pkt.symbolTable && pkt.symbolCode) { - const symbol = escapeAprsHtml(pkt.symbolCode); - const table = pkt.symbolTable === "/" ? "Primary" : "Alternate"; - symbolHtml = `${symbol}`; - } + const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml); const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : ""; @@ -450,22 +330,7 @@ function addAprsPacket(pkt: AprsPacket): void { } function normalizeServerAprsPacket(pkt: AprsPacket): AprsPacket { - return { - rig_id: pkt.rig_id || null, - receiver: aprsWindow.getDecodeRigMeta?.() ?? null, - srcCall: pkt.src_call ?? "", - destCall: pkt.dest_call ?? "", - path: pkt.path ?? "", - info: pkt.info ?? "", - info_bytes: pkt.info_bytes ?? [], - type: pkt.packet_type ?? "", - crcOk: pkt.crc_ok ?? false, - ts_ms: pkt.ts_ms ?? null, - lat: pkt.lat ?? null, - lon: pkt.lon ?? null, - symbolTable: pkt.symbol_table ?? null, - symbolCode: pkt.symbol_code ?? null, - }; + return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null); } aprsWindow.onServerAprsBatch = function(packets: AprsPacket[]) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts similarity index 51% rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.js rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts index 0b5c9dca..47109b5b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts @@ -2,10 +2,48 @@ // // SPDX-License-Identifier: GPL-2.0-or-later +import { + aprsAgeText, + aprsCategoryLabel, + aprsHexBytes, + aprsPacketCategory, + collapseAprsDuplicates, + normalizeAprsPacket, + renderAprsInfo, + renderLocalAprsSymbol, + type AprsPacket, + type AprsTypeFilter, +} from "./aprs-shared"; + +interface HfAprsBridge { + getDecodeHistoryRetentionMs?: () => number; + trxScheduleUiFrameJob?: (key: string, job: () => void) => void; + serverLat?: number | null; + serverLon?: number | null; + haversineKm?: (lat1: number, lon1: number, lat2: number, lon2: number) => number; + escapeMapHtml?: (input: string) => string; + navigateToAprsMap?: (lat: number, lon: number) => void; + showHint?: (message: string, durationMs: number) => void; + getDecodeRigMeta?: () => unknown; + takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise; + postPath?: (path: string) => Promise; + trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise }; + resetHfAprsHistoryView?: () => void; + pruneHfAprsHistoryView?: () => void; + onServerHfAprsBatch?: (packets: AprsPacket[]) => void; + restoreHfAprsHistory?: (packets: AprsPacket[]) => void; + onServerHfAprs?: (packet: AprsPacket) => void; + _trxDrainPendingDecode?: (decoder: string) => void; +} +const hfAprsWindow = window as unknown as HfAprsBridge; +const escapeHfAprsHtml = (input: string): string => hfAprsWindow.escapeMapHtml?.(input) ?? input + .replaceAll("&", "&").replaceAll("<", "<") + .replaceAll(">", ">").replaceAll('"', """); + // --- 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 hfAprsFilterInput = document.getElementById("hf-aprs-filter") as HTMLInputElement | null; 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"); @@ -13,92 +51,43 @@ 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 hfAprsPacketHistory: AprsPacket[] = []; let hfAprsOnlyPos = false; let hfAprsHideCrc = false; let hfAprsCollapseDup = false; -let hfAprsTypeFilter = "all"; +let hfAprsTypeFilter: AprsTypeFilter = "all"; -function currentHfAprsHistoryRetentionMs() { - return typeof window.getDecodeHistoryRetentionMs === "function" - ? window.getDecodeHistoryRetentionMs() +function currentHfAprsHistoryRetentionMs(): number { + return typeof hfAprsWindow.getDecodeHistoryRetentionMs === "function" + ? hfAprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1000; } function pruneHfAprsPacketHistory() { const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs(); - hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => Number(pkt?._tsMs) >= cutoffMs); + hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs); } function scheduleHfAprsHistoryRender() { - if (typeof window.trxScheduleUiFrameJob === "function") { - window.trxScheduleUiFrameJob("hf-aprs-history", () => renderHfAprsHistory()); + if (typeof hfAprsWindow.trxScheduleUiFrameJob === "function") { + hfAprsWindow.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); +function hfAprsDistanceText(pkt: AprsPacket): string { + if (hfAprsWindow.serverLat == null || hfAprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !hfAprsWindow.haversineKm) return ""; + const distKm = hfAprsWindow.haversineKm(hfAprsWindow.serverLat, hfAprsWindow.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) { +function hfAprsFilterMatch(pkt: AprsPacket): boolean { 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 (hfAprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== hfAprsTypeFilter) return false; if (!hfAprsFilterText) return true; const haystack = [ pkt.srcCall, @@ -108,7 +97,7 @@ function hfAprsFilterMatch(pkt) { pkt.type, pkt.lat != null ? pkt.lat.toFixed(4) : "", pkt.lon != null ? pkt.lon.toFixed(4) : "", - hfAprsPacketCategory(pkt), + aprsPacketCategory(pkt), ] .filter(Boolean) .join(" ") @@ -116,22 +105,12 @@ function hfAprsFilterMatch(pkt) { return haystack.includes(hfAprsFilterText); } -function hfAprsVisiblePackets() { +function hfAprsVisiblePackets(): AprsPacket[] { 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; -} +const collapseHfAprsDuplicates = collapseAprsDuplicates; function updateHfAprsSummary() { const visible = hfAprsVisiblePackets(); @@ -146,7 +125,7 @@ function updateHfAprsSummary() { if (!latest) { hfAprsLatestSeenEl.textContent = "No packets yet"; } else { - hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${hfAprsAgeText(latest._tsMs)}`; + hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`; } } } @@ -160,69 +139,21 @@ function updateHfAprsChipState() { 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) { +function renderHfAprsRow(pkt: AprsPacket, isFresh: boolean): HTMLElement { 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 age = aprsAgeText(pkt._tsMs); + const category = aprsPacketCategory(pkt); + const categoryLabel = aprsCategoryLabel(category); const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`; - const pathBadge = pkt.path ? `${escapeMapHtml(pkt.path)}` : ""; + const pathBadge = pkt.path ? `${escapeHfAprsHtml(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 symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml); const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : ""; @@ -234,19 +165,19 @@ function renderHfAprsRow(pkt, isFresh) { `${ts}` + hfBadge + symbolHtml + - `${escapeMapHtml(pkt.srcCall)}` + - `>${escapeMapHtml(pkt.destCall || "")}` + - `${escapeMapHtml(categoryLabel)}` + + `${escapeHfAprsHtml(pkt.srcCall ?? "")}` + + `>${escapeHfAprsHtml(pkt.destCall || "")}` + + `${escapeHfAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `` + `
` + - `${escapeMapHtml(age)}` + - (distance ? `${escapeMapHtml(distance)}` : "") + - `${escapeMapHtml(pkt.type || "--")}` + + `${escapeHfAprsHtml(age)}` + + (distance ? `${escapeHfAprsHtml(distance)}` : "") + + `${escapeHfAprsHtml(pkt.type || "--")}` + `
` + `
` + - `${renderHfAprsInfo(pkt)}` + + `${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + `
` + @@ -257,42 +188,43 @@ function renderHfAprsRow(pkt, isFresh) { `
` + `Details` + `
` + - `Source${escapeMapHtml(pkt.srcCall || "--")}` + - `Destination${escapeMapHtml(pkt.destCall || "--")}` + - `Type${escapeMapHtml(pkt.type || "--")}` + - `Path${escapeMapHtml(pkt.path || "--")}` + - `Age${escapeMapHtml(age)}` + + `Source${escapeHfAprsHtml(pkt.srcCall || "--")}` + + `Destination${escapeHfAprsHtml(pkt.destCall || "--")}` + + `Type${escapeHfAprsHtml(pkt.type || "--")}` + + `Path${escapeHfAprsHtml(pkt.path || "--")}` + + `Age${escapeHfAprsHtml(age)}` + `CRC${pkt.crcOk ? "OK" : "Failed"}` + `Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}` + - `Info${escapeMapHtml(pkt.info || "--")}` + - `Info Bytes${escapeMapHtml(hfAprsHexBytes(pkt.info_bytes))}` + + `Info${escapeHfAprsHtml(pkt.info || "--")}` + + `Info Bytes${escapeHfAprsHtml(aprsHexBytes(pkt.info_bytes))}` + `
` + `
`; - row.querySelectorAll("[data-aprs-map]").forEach((el) => { + row.querySelectorAll("[data-aprs-map]").forEach((el) => { el.addEventListener("click", (evt) => { evt.preventDefault(); - const raw = String(el.dataset.aprsMap || ""); + const raw = el.dataset.aprsMap ?? ""; const [lat, lon] = raw.split(",").map(Number); - if (window.navigateToAprsMap && Number.isFinite(lat) && Number.isFinite(lon)) { - window.navigateToAprsMap(lat, lon); + if (hfAprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) { + hfAprsWindow.navigateToAprsMap(lat, lon); } }); }); - const copyBtn = row.querySelector("[data-aprs-copy]"); + const copyBtn = row.querySelector("[data-aprs-copy]"); if (copyBtn) { - copyBtn.addEventListener("click", async () => { - const raw = String(copyBtn.dataset.aprsCopy || ""); + copyBtn.addEventListener("click", () => { void (async () => { + const raw = copyBtn.dataset.aprsCopy ?? ""; try { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(raw); - showHint("Coordinates copied", 1200); + const clipboard = Reflect.get(navigator, "clipboard") as Clipboard | undefined; + if (clipboard) { + await clipboard.writeText(raw); + hfAprsWindow.showHint?.("Coordinates copied", 1200); } - } catch (_e) { - showHint("Copy failed", 1500); + } catch { + hfAprsWindow.showHint?.("Copy failed", 1500); } - }); + })(); }); } return row; @@ -307,26 +239,26 @@ function renderHfAprsHistory() { } const visible = hfAprsVisiblePackets(); const fragment = document.createDocumentFragment(); - for (let i = 0; i < visible.length; i++) { - fragment.appendChild(renderHfAprsRow(visible[i], i === 0)); + for (const [index, packet] of visible.entries()) { + fragment.appendChild(renderHfAprsRow(packet, index === 0)); } hfAprsPacketsEl.replaceChildren(fragment); updateHfAprsSummary(); updateHfAprsChipState(); } -window.resetHfAprsHistoryView = function() { +hfAprsWindow.resetHfAprsHistoryView = function() { if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = ""; hfAprsPacketHistory = []; renderHfAprsHistory(); }; -window.pruneHfAprsHistoryView = function() { +hfAprsWindow.pruneHfAprsHistoryView = function() { pruneHfAprsPacketHistory(); renderHfAprsHistory(); }; -function addHfAprsPacket(pkt) { +function addHfAprsPacket(pkt: AprsPacket): void { const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now(); pkt._tsMs = tsMs; pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); @@ -337,29 +269,14 @@ function addHfAprsPacket(pkt) { scheduleHfAprsHistoryRender(); } -function normalizeServerHfAprsPacket(pkt) { - return { - rig_id: pkt.rig_id || null, - receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null, - srcCall: pkt.src_call, - destCall: pkt.dest_call, - path: pkt.path, - info: pkt.info, - info_bytes: pkt.info_bytes, - type: pkt.packet_type, - crcOk: pkt.crc_ok, - ts_ms: pkt.ts_ms, - lat: pkt.lat, - lon: pkt.lon, - symbolTable: pkt.symbol_table, - symbolCode: pkt.symbol_code, - }; +function normalizeServerHfAprsPacket(pkt: AprsPacket): AprsPacket { + return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null); } -window.onServerHfAprsBatch = function(packets) { +hfAprsWindow.onServerHfAprsBatch = function(packets: AprsPacket[]) { if (!Array.isArray(packets) || packets.length === 0) return; if (hfAprsStatus) hfAprsStatus.textContent = "Receiving"; - const normalized = []; + const normalized: AprsPacket[] = []; for (const pkt of packets) { const next = normalizeServerHfAprsPacket(pkt); const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now(); @@ -373,29 +290,29 @@ window.onServerHfAprsBatch = function(packets) { scheduleHfAprsHistoryRender(); }; -window.restoreHfAprsHistory = function(packets) { - window.onServerHfAprsBatch(packets); +hfAprsWindow.restoreHfAprsHistory = function(packets: AprsPacket[]) { + hfAprsWindow.onServerHfAprsBatch?.(packets); }; const hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn"); -hfAprsDecodeToggleBtn?.addEventListener("click", async () => { +hfAprsDecodeToggleBtn?.addEventListener("click", () => { void (async () => { try { - await window.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn); - await postPath("/toggle_hf_aprs_decode"); + await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn); + await hfAprsWindow.postPath?.("/toggle_hf_aprs_decode"); } catch (e) { console.error("HF APRS toggle failed", e); } -}); +})(); }); -document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", async () => { - if (!await window.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return; +document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", () => { void (async () => { + if (!await hfAprsWindow.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return; try { - await postPath("/clear_hf_aprs_decode"); - window.resetHfAprsHistoryView(); + await hfAprsWindow.postPath?.("/clear_hf_aprs_decode"); + hfAprsWindow.resetHfAprsHistoryView?.(); } catch (e) { console.error("HF APRS history clear failed", e); } -}); +})(); }); if (hfAprsOnlyPosBtn) { hfAprsOnlyPosBtn.addEventListener("click", () => { @@ -418,7 +335,7 @@ if (hfAprsCollapseDupBtn) { }); } -["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => { +(["all", "position", "message", "weather", "telemetry", "other"] as const).forEach((type) => { const btn = document.getElementById(`hf-aprs-type-${type}`); if (!btn) return; btn.addEventListener("click", () => { @@ -435,10 +352,10 @@ if (hfAprsFilterInput) { } // --- Server-side HF APRS decode handler --- -window.onServerHfAprs = function(pkt) { +hfAprsWindow.onServerHfAprs = function(pkt: AprsPacket) { if (hfAprsStatus) hfAprsStatus.textContent = "Receiving"; addHfAprsPacket(normalizeServerHfAprsPacket(pkt)); }; renderHfAprsHistory(); -if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("hf_aprs"); +hfAprsWindow._trxDrainPendingDecode?.("hf_aprs"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs new file mode 100644 index 00000000..9c729b73 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs @@ -0,0 +1,31 @@ +// 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("HF APRS entry uses shared typed normalization and local symbols", async () => { + const window = { trxUi: { confirm: async () => true } }; + const context = vm.createContext({ + window, + document: { getElementById: () => null, querySelectorAll: () => [] }, + navigator: {}, + Date, + Number, + String, + Array, + Set, + Reflect, + console, + }); + const source = await readFile(new URL("../../assets/web/generated/hf-aprs.js", import.meta.url), "utf8"); + assert.equal(source.includes("raw.githubusercontent.com"), false); + new vm.Script(source).runInContext(context); + + window.onServerHfAprs({ src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, symbol_table: "/", symbol_code: ">" }); + assert.equal(typeof window.restoreHfAprsHistory, "function"); + assert.equal(typeof window.pruneHfAprsHistoryView, "function"); +});