CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m12s
CI / frontend (pull_request) Successful in 3m2s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 7m28s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 3s
Resolve a table/code pair to a sprite cell in aprs-shared, and use it from both the packet lists and the map markers, which had each been printing the raw symbol character in a bordered box. A table identifier of / or \ selects the primary or alternate sheet directly. Anything else is an overlay character, which the APRS spec draws on top of the alternate symbol -- so those stack the overlay sheet over the alternate one rather than picking a sheet. Codes outside 0x21..0x7E have no cell and keep the old character box. The sheet URLs stay in the stylesheet so a min-resolution query can swap in the retina sheets; only the cell offset is computed and set inline. Map markers share the helper through the plugin chunk, so the map stays free of any remote symbol fetch. Verified in a browser against the real stylesheet and sheets: /> is a car, /_ a WX circle, /& an igate diamond, \n a red triangle, and the overlays S> and 7# carry their character on the alternate symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018huL1ELyr86yVqfAabtioA Signed-off-by: Stan Grams <sjg@haxx.space>
270 lines
12 KiB
JavaScript
270 lines
12 KiB
JavaScript
import {
|
|
aprsAgeText,
|
|
aprsCategoryLabel,
|
|
aprsHexBytes,
|
|
aprsPacketCategory,
|
|
collapseAprsDuplicates,
|
|
normalizeAprsPacket,
|
|
renderAprsInfo,
|
|
renderLocalAprsSymbol
|
|
} from "./chunk-ROTCLFXS.js";
|
|
import {
|
|
hostCore,
|
|
hostState
|
|
} from "./chunk-KL66PICH.js";
|
|
|
|
// src/plugins/hf-aprs.ts
|
|
var hfAprsWindow = window;
|
|
var escapeHfAprsHtml = (input) => hostCore.escapeMapHtml(input);
|
|
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 (hostState.serverLat == null || hostState.serverLon == null || pkt.lat == null || pkt.lon == null) return "";
|
|
const distKm = hostCore.haversineKm(hostState.serverLat, hostState.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 {
|
|
hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
|
|
}
|
|
}
|
|
}
|
|
function updateHfAprsChipState() {
|
|
document.querySelectorAll("[id^='hf-aprs-type-']").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.id === `hf-aprs-type-${hfAprsTypeFilter}`);
|
|
});
|
|
hfAprsOnlyPosBtn?.classList.toggle("active", hfAprsOnlyPos);
|
|
hfAprsHideCrcBtn?.classList.toggle("active", hfAprsHideCrc);
|
|
hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup);
|
|
}
|
|
function 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 ? `<span class="aprs-badge">${escapeHfAprsHtml(pkt.path)}</span>` : "";
|
|
const crcBadge = pkt.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC Fail</span>';
|
|
const hfBadge = '<span class="aprs-badge" style="background:var(--accent-alt,#f59e0b);color:#000">HF</span>';
|
|
const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml);
|
|
const posLink = pkt.lat != null && pkt.lon != null ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${pkt.lat},${pkt.lon}">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>` : "";
|
|
const distance = hfAprsDistanceText(pkt);
|
|
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
|
|
row.innerHTML = `<div class="aprs-row-head"><span class="aprs-time">${ts}</span>` + hfBadge + symbolHtml + `<span class="aprs-call">${escapeHfAprsHtml(pkt.srcCall ?? "")}</span><span>>${escapeHfAprsHtml(pkt.destCall || "")}</span><span class="${categoryClass}">${escapeHfAprsHtml(categoryLabel)}</span>` + pathBadge + crcBadge + `</div><div class="aprs-row-meta"><span class="aprs-meta-text">${escapeHfAprsHtml(age)}</span>` + (distance ? `<span class="aprs-meta-text">${escapeHfAprsHtml(distance)}</span>` : "") + `<span class="aprs-meta-text">${escapeHfAprsHtml(pkt.type || "--")}</span></div><div class="aprs-row-detail"><span title="${escapeHfAprsHtml(pkt.type || "")}">${renderAprsInfo(pkt)}</span>` + (posLink ? `<span>${posLink}</span>` : "") + `</div><div class="aprs-row-actions">` + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${pkt.lat},${pkt.lon}">Map</button>` : "") + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${pkt.lat},${pkt.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div><details class="aprs-details"><summary>Details</summary><div class="aprs-details-grid"><span class="aprs-detail-label">Source</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.srcCall || "--")}</span><span class="aprs-detail-label">Destination</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.destCall || "--")}</span><span class="aprs-detail-label">Type</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.type || "--")}</span><span class="aprs-detail-label">Path</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.path || "--")}</span><span class="aprs-detail-label">Age</span><span class="aprs-detail-value">${escapeHfAprsHtml(age)}</span><span class="aprs-detail-label">CRC</span><span class="aprs-detail-value">${pkt.crcOk ? "OK" : "Failed"}</span><span class="aprs-detail-label">Position</span><span class="aprs-detail-value">${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}</span><span class="aprs-detail-label">Info</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.info || "--")}</span><span class="aprs-detail-label">Info Bytes</span><span class="aprs-detail-value">${escapeHfAprsHtml(aprsHexBytes(pkt.info_bytes))}</span></div></details>`;
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
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);
|
|
hostCore.showHint("Coordinates copied", 1200);
|
|
}
|
|
} catch {
|
|
hostCore.showHint("Copy failed", 1500);
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
return row;
|
|
}
|
|
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();
|
|
}
|
|
function resetHfAprsHistoryView() {
|
|
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
|
hfAprsPacketHistory = [];
|
|
renderHfAprsHistory();
|
|
}
|
|
function pruneHfAprsHistoryView() {
|
|
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 normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
|
|
}
|
|
function onServerHfAprsBatch(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();
|
|
}
|
|
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
|
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
|
|
void (async () => {
|
|
try {
|
|
await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn);
|
|
await hostCore.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 hostCore.postPath("/clear_hf_aprs_decode");
|
|
resetHfAprsHistoryView();
|
|
} catch (e) {
|
|
console.error("HF APRS history clear failed", e);
|
|
}
|
|
})();
|
|
});
|
|
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 (hfAprsFilterInput) {
|
|
hfAprsFilterInput.addEventListener("input", () => {
|
|
hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase();
|
|
renderHfAprsHistory();
|
|
});
|
|
}
|
|
function onServerHfAprs(pkt) {
|
|
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
|
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
|
}
|
|
renderHfAprsHistory();
|
|
window.trxPluginRuntime.registerDecoder({
|
|
id: "hf_aprs",
|
|
onMessage: onServerHfAprs,
|
|
onBatch: onServerHfAprsBatch,
|
|
restore: onServerHfAprsBatch,
|
|
reset: resetHfAprsHistoryView,
|
|
prune: pruneHfAprsHistoryView
|
|
});
|