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 ? `
` : "") + `
QRZDetails
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 ? `
` : "") + `
QRZDetails
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");
+});