Complete TypeScript frontend migration #22
@@ -1,48 +1,46 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/ais.ts
|
||||
var aisWindow = window;
|
||||
var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
var aisStatus = document.getElementById("ais-status");
|
||||
var aisMessagesEl = document.getElementById("ais-messages");
|
||||
var aisFilterInput = document.getElementById("ais-filter");
|
||||
var aisBarOverlay = document.getElementById("ais-bar-overlay");
|
||||
var aisChannelSummaryEl = document.getElementById("ais-channel-summary");
|
||||
var aisVesselCountEl = document.getElementById("ais-vessel-count");
|
||||
var aisLatestSeenEl = document.getElementById("ais-latest-seen");
|
||||
var AIS_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var AIS_DEFAULT_A_HZ = 161975e3;
|
||||
var AIS_CHANNEL_SPACING_HZ = 5e4;
|
||||
var aisFilterText = "";
|
||||
var aisMessageHistory = [];
|
||||
function currentAisHistoryRetentionMs() {
|
||||
// src/plugins/ais.ts
|
||||
var aisWindow = window;
|
||||
var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
var aisStatus = document.getElementById("ais-status");
|
||||
var aisMessagesEl = document.getElementById("ais-messages");
|
||||
var aisFilterInput = document.getElementById("ais-filter");
|
||||
var aisBarOverlay = document.getElementById("ais-bar-overlay");
|
||||
var aisChannelSummaryEl = document.getElementById("ais-channel-summary");
|
||||
var aisVesselCountEl = document.getElementById("ais-vessel-count");
|
||||
var aisLatestSeenEl = document.getElementById("ais-latest-seen");
|
||||
var AIS_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var AIS_DEFAULT_A_HZ = 161975e3;
|
||||
var AIS_CHANNEL_SPACING_HZ = 5e4;
|
||||
var aisFilterText = "";
|
||||
var aisMessageHistory = [];
|
||||
function currentAisHistoryRetentionMs() {
|
||||
return typeof aisWindow.getDecodeHistoryRetentionMs === "function" ? aisWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
|
||||
}
|
||||
function pruneAisMessageHistory() {
|
||||
}
|
||||
function pruneAisMessageHistory() {
|
||||
const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
|
||||
aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
|
||||
}
|
||||
function scheduleAisUi(key, job) {
|
||||
}
|
||||
function scheduleAisUi(key, job) {
|
||||
if (typeof aisWindow.trxScheduleUiFrameJob === "function") {
|
||||
aisWindow.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
function scheduleAisHistoryRender() {
|
||||
}
|
||||
function scheduleAisHistoryRender() {
|
||||
scheduleAisUi("ais-history", () => {
|
||||
renderAisHistory();
|
||||
});
|
||||
}
|
||||
function scheduleAisBarUpdate() {
|
||||
}
|
||||
function scheduleAisBarUpdate() {
|
||||
scheduleAisUi("ais-bar", () => {
|
||||
updateAisBar();
|
||||
});
|
||||
}
|
||||
function formatAisMhz(freqHz) {
|
||||
}
|
||||
function formatAisMhz(freqHz) {
|
||||
return `${(freqHz / 1e6).toFixed(3)} MHz`;
|
||||
}
|
||||
function currentAisChannelPlan() {
|
||||
}
|
||||
function currentAisChannelPlan() {
|
||||
const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
|
||||
const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ;
|
||||
const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ;
|
||||
@@ -50,8 +48,8 @@
|
||||
aHz: safeAHz,
|
||||
bHz: safeAHz + AIS_CHANNEL_SPACING_HZ
|
||||
};
|
||||
}
|
||||
function aisChannelInfo(channel) {
|
||||
}
|
||||
function aisChannelInfo(channel) {
|
||||
const plan = currentAisChannelPlan();
|
||||
const ch = (channel ?? "").trim().toUpperCase();
|
||||
if (ch === "B") {
|
||||
@@ -66,17 +64,17 @@
|
||||
badgeClass: "ais-badge ais-badge-channel-a",
|
||||
freqText: formatAisMhz(plan.aHz)
|
||||
};
|
||||
}
|
||||
function aisDisplayName(msg) {
|
||||
}
|
||||
function aisDisplayName(msg) {
|
||||
return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`;
|
||||
}
|
||||
function aisDisplayNameHtml(msg) {
|
||||
}
|
||||
function aisDisplayNameHtml(msg) {
|
||||
const label = escapeAisHtml(aisDisplayName(msg));
|
||||
const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null;
|
||||
if (!url) return label;
|
||||
return `<a class="title-link" href="${escapeAisHtml(url)}" target="_blank" rel="noopener">${label}</a>`;
|
||||
}
|
||||
function aisTypeLabel(type) {
|
||||
}
|
||||
function aisTypeLabel(type) {
|
||||
switch (Number(type)) {
|
||||
case 1:
|
||||
case 2:
|
||||
@@ -97,8 +95,8 @@
|
||||
default:
|
||||
return `Type ${type ?? "--"}`;
|
||||
}
|
||||
}
|
||||
function aisAgeText(tsMs) {
|
||||
}
|
||||
function aisAgeText(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);
|
||||
@@ -108,19 +106,19 @@
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
function aisMotionText(msg) {
|
||||
}
|
||||
function aisMotionText(msg) {
|
||||
const parts = [
|
||||
msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
|
||||
msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}° COG` : null,
|
||||
msg.heading_deg != null ? `${msg.heading_deg.toFixed(0)}° HDG` : null
|
||||
].filter(Boolean);
|
||||
return parts.join(" · ");
|
||||
}
|
||||
function aisRouteText(msg) {
|
||||
}
|
||||
function aisRouteText(msg) {
|
||||
return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
|
||||
}
|
||||
function aisDistanceText(msg) {
|
||||
}
|
||||
function aisDistanceText(msg) {
|
||||
if (aisWindow.serverLat == null || aisWindow.serverLon == null || msg.lat == null || msg.lon == null || !aisWindow.haversineKm) {
|
||||
return "";
|
||||
}
|
||||
@@ -128,16 +126,16 @@
|
||||
if (!Number.isFinite(distKm)) return "";
|
||||
if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
|
||||
return `${distKm.toFixed(1)} km from TRX`;
|
||||
}
|
||||
function aisLatestByVessel(messages) {
|
||||
}
|
||||
function aisLatestByVessel(messages) {
|
||||
const byMmsi = /* @__PURE__ */ new Map();
|
||||
for (const msg of messages) {
|
||||
const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`;
|
||||
if (!byMmsi.has(key)) byMmsi.set(key, msg);
|
||||
}
|
||||
return Array.from(byMmsi.values());
|
||||
}
|
||||
function updateAisSummary() {
|
||||
}
|
||||
function updateAisSummary() {
|
||||
const plan = currentAisChannelPlan();
|
||||
if (aisChannelSummaryEl) {
|
||||
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
|
||||
@@ -156,8 +154,8 @@
|
||||
aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function renderAisRow(msg) {
|
||||
}
|
||||
function renderAisRow(msg) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ais-message";
|
||||
const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
||||
@@ -185,16 +183,16 @@
|
||||
row.innerHTML = `<div class="ais-row-head"><span class="ais-time">${ts}</span><span class="ais-call">${nameHtml}</span><span class="${channel.badgeClass}">${escapeAisHtml(channel.label)}</span><span class="ais-badge ais-badge-type">${escapeAisHtml(aisTypeLabel(msg.message_type))}</span></div><div class="ais-row-meta"><span>MMSI ${escapeAisHtml(String(msg.mmsi))}</span>` + (route ? `<span class="ais-meta-text">${escapeAisHtml(route)}</span>` : "") + `<span class="ais-meta-text">${escapeAisHtml(channel.freqText)}</span></div><div class="ais-row-detail">` + (motion ? `<span>${escapeAisHtml(motion)}</span>` : `<span>No motion data</span>`) + (distance ? `<span>${escapeAisHtml(distance)}</span>` : "") + (pos ? `<span>${pos}</span>` : "") + `<span>${escapeAisHtml(aisAgeText(msg._tsMs))}</span></div>`;
|
||||
applyAisFilterToRow(row);
|
||||
return row;
|
||||
}
|
||||
function applyAisFilterToRow(row) {
|
||||
}
|
||||
function applyAisFilterToRow(row) {
|
||||
if (!aisFilterText) {
|
||||
row.style.display = "";
|
||||
return;
|
||||
}
|
||||
const message = row.dataset.filterText || "";
|
||||
row.style.display = message.includes(aisFilterText) ? "" : "none";
|
||||
}
|
||||
function updateAisBar() {
|
||||
}
|
||||
function updateAisBar() {
|
||||
if (!aisBarOverlay) return;
|
||||
updateAisSummary();
|
||||
const isAis = (document.getElementById("mode")?.value || "").toUpperCase() === "AIS";
|
||||
@@ -225,19 +223,19 @@
|
||||
}
|
||||
aisBarOverlay.innerHTML = html;
|
||||
aisBarOverlay.style.display = "flex";
|
||||
}
|
||||
aisWindow.updateAisBar = updateAisBar;
|
||||
aisWindow.clearAisBar = function() {
|
||||
}
|
||||
aisWindow.updateAisBar = updateAisBar;
|
||||
aisWindow.clearAisBar = function() {
|
||||
resetAisHistoryView();
|
||||
};
|
||||
function resetAisHistoryView() {
|
||||
};
|
||||
function resetAisHistoryView() {
|
||||
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
|
||||
aisMessageHistory = [];
|
||||
updateAisBar();
|
||||
renderAisHistory();
|
||||
aisWindow.clearMapMarkersByType?.("ais");
|
||||
}
|
||||
function renderAisHistory() {
|
||||
}
|
||||
function renderAisHistory() {
|
||||
pruneAisMessageHistory();
|
||||
if (!aisMessagesEl) {
|
||||
updateAisSummary();
|
||||
@@ -249,8 +247,8 @@
|
||||
}
|
||||
aisMessagesEl.replaceChildren(fragment);
|
||||
updateAisSummary();
|
||||
}
|
||||
function addAisMessage(msg) {
|
||||
}
|
||||
function addAisMessage(msg) {
|
||||
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||
msg._tsMs = tsMs;
|
||||
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||
@@ -265,14 +263,14 @@
|
||||
if (msg.lat != null && msg.lon != null && aisWindow.aisMapAddVessel) {
|
||||
aisWindow.aisMapAddVessel(msg);
|
||||
}
|
||||
}
|
||||
function normalizeServerAisMessage(msg) {
|
||||
}
|
||||
function normalizeServerAisMessage(msg) {
|
||||
return {
|
||||
...msg,
|
||||
rig_id: msg.rig_id || null
|
||||
};
|
||||
}
|
||||
function onServerAisBatch(messages) {
|
||||
}
|
||||
function onServerAisBatch(messages) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||
const normalized = [];
|
||||
@@ -295,13 +293,13 @@
|
||||
pruneAisMessageHistory();
|
||||
scheduleAisBarUpdate();
|
||||
scheduleAisHistoryRender();
|
||||
}
|
||||
function pruneAisHistoryView() {
|
||||
}
|
||||
function pruneAisHistoryView() {
|
||||
pruneAisMessageHistory();
|
||||
updateAisBar();
|
||||
renderAisHistory();
|
||||
}
|
||||
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
|
||||
}
|
||||
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
@@ -311,24 +309,23 @@
|
||||
console.error("AIS history clear failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
if (aisFilterInput) {
|
||||
});
|
||||
if (aisFilterInput) {
|
||||
aisFilterInput.addEventListener("input", () => {
|
||||
aisFilterText = aisFilterInput.value.trim().toUpperCase();
|
||||
renderAisHistory();
|
||||
});
|
||||
}
|
||||
function onServerAis(msg) {
|
||||
}
|
||||
function onServerAis(msg) {
|
||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||
addAisMessage(normalizeServerAisMessage(msg));
|
||||
}
|
||||
updateAisSummary();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
updateAisSummary();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
id: "ais",
|
||||
onMessage: onServerAis,
|
||||
onBatch: onServerAisBatch,
|
||||
restore: onServerAisBatch,
|
||||
reset: resetAisHistoryView,
|
||||
prune: pruneAisHistoryView
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -1,163 +1,70 @@
|
||||
"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)) : `<span class="aprs-byte">0x${byte.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
||||
}
|
||||
function renderAprsCharacter(character) {
|
||||
const code = character.charCodeAt(0);
|
||||
return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `<span class="aprs-byte">0x${code.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
||||
}
|
||||
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 `<span class="aprs-symbol aprs-symbol-local" title="${table} APRS symbol ${symbol}">${symbol}</span>`;
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
import {
|
||||
aprsAgeText,
|
||||
aprsCategoryLabel,
|
||||
aprsHexBytes,
|
||||
aprsPacketCategory,
|
||||
collapseAprsDuplicates,
|
||||
normalizeAprsPacket,
|
||||
renderAprsInfo,
|
||||
renderLocalAprsSymbol
|
||||
} from "./chunk-M2I6DH4X.js";
|
||||
|
||||
// src/plugins/aprs.ts
|
||||
var aprsWindow = window;
|
||||
var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
var showAprsHint = (message, durationMs) => {
|
||||
// src/plugins/aprs.ts
|
||||
var aprsWindow = window;
|
||||
var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
var showAprsHint = (message, durationMs) => {
|
||||
aprsWindow.showHint?.(message, durationMs);
|
||||
};
|
||||
var aprsStatus = document.getElementById("aprs-status");
|
||||
var aprsPacketsEl = document.getElementById("aprs-packets");
|
||||
var aprsFilterInput = document.getElementById("aprs-filter");
|
||||
var aprsBarOverlay = document.getElementById("aprs-bar-overlay");
|
||||
var aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
|
||||
var aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
|
||||
var aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
|
||||
var aprsTotalCountEl = document.getElementById("aprs-total-count");
|
||||
var aprsVisibleCountEl = document.getElementById("aprs-visible-count");
|
||||
var aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
|
||||
var APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var aprsFilterText = "";
|
||||
var aprsPacketHistory = [];
|
||||
var aprsBarDismissedAtMs = 0;
|
||||
var aprsOnlyPos = false;
|
||||
var aprsHideCrc = false;
|
||||
var aprsCollapseDup = false;
|
||||
var aprsTypeFilter = "all";
|
||||
function currentAprsHistoryRetentionMs() {
|
||||
};
|
||||
var aprsStatus = document.getElementById("aprs-status");
|
||||
var aprsPacketsEl = document.getElementById("aprs-packets");
|
||||
var aprsFilterInput = document.getElementById("aprs-filter");
|
||||
var aprsBarOverlay = document.getElementById("aprs-bar-overlay");
|
||||
var aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
|
||||
var aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
|
||||
var aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
|
||||
var aprsTotalCountEl = document.getElementById("aprs-total-count");
|
||||
var aprsVisibleCountEl = document.getElementById("aprs-visible-count");
|
||||
var aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
|
||||
var APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var aprsFilterText = "";
|
||||
var aprsPacketHistory = [];
|
||||
var aprsBarDismissedAtMs = 0;
|
||||
var aprsOnlyPos = false;
|
||||
var aprsHideCrc = false;
|
||||
var aprsCollapseDup = false;
|
||||
var aprsTypeFilter = "all";
|
||||
function currentAprsHistoryRetentionMs() {
|
||||
return typeof aprsWindow.getDecodeHistoryRetentionMs === "function" ? aprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
|
||||
}
|
||||
function pruneAprsPacketHistory() {
|
||||
}
|
||||
function pruneAprsPacketHistory() {
|
||||
const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
|
||||
aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
|
||||
}
|
||||
function scheduleAprsUi(key, job) {
|
||||
}
|
||||
function scheduleAprsUi(key, job) {
|
||||
if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
|
||||
aprsWindow.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
function scheduleAprsHistoryRender() {
|
||||
}
|
||||
function scheduleAprsHistoryRender() {
|
||||
scheduleAprsUi("aprs-history", () => {
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
function scheduleAprsBarUpdate() {
|
||||
}
|
||||
function scheduleAprsBarUpdate() {
|
||||
scheduleAprsUi("aprs-bar", () => {
|
||||
updateAprsBar();
|
||||
});
|
||||
}
|
||||
function aprsDistanceText(pkt) {
|
||||
}
|
||||
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);
|
||||
if (!Number.isFinite(distKm)) return "";
|
||||
if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
|
||||
return `${distKm.toFixed(1)} km from TRX`;
|
||||
}
|
||||
function aprsFilterMatch(pkt) {
|
||||
}
|
||||
function aprsFilterMatch(pkt) {
|
||||
if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
|
||||
if (aprsHideCrc && !pkt.crcOk) return false;
|
||||
if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
|
||||
@@ -173,12 +80,12 @@
|
||||
aprsPacketCategory(pkt)
|
||||
].filter(Boolean).join(" ").toUpperCase();
|
||||
return haystack.includes(aprsFilterText);
|
||||
}
|
||||
function aprsVisiblePackets() {
|
||||
}
|
||||
function aprsVisiblePackets() {
|
||||
const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
|
||||
return packets.filter(aprsFilterMatch);
|
||||
}
|
||||
function updateAprsSummary() {
|
||||
}
|
||||
function updateAprsSummary() {
|
||||
const visible = aprsVisiblePackets();
|
||||
if (aprsTotalCountEl) {
|
||||
aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
|
||||
@@ -194,16 +101,16 @@
|
||||
aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateAprsChipState() {
|
||||
}
|
||||
function updateAprsChipState() {
|
||||
document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
|
||||
btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
|
||||
});
|
||||
aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
|
||||
aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
|
||||
aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
|
||||
}
|
||||
function renderAprsRow(pkt, isFresh) {
|
||||
}
|
||||
function renderAprsRow(pkt, isFresh) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "aprs-packet";
|
||||
if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
|
||||
@@ -248,8 +155,8 @@
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}
|
||||
function renderAprsHistory() {
|
||||
}
|
||||
function renderAprsHistory() {
|
||||
pruneAprsPacketHistory();
|
||||
if (!aprsPacketsEl) {
|
||||
updateAprsSummary();
|
||||
@@ -264,8 +171,8 @@
|
||||
aprsPacketsEl.replaceChildren(fragment);
|
||||
updateAprsSummary();
|
||||
updateAprsChipState();
|
||||
}
|
||||
function updateAprsBar() {
|
||||
}
|
||||
function updateAprsBar() {
|
||||
if (!aprsBarOverlay) return;
|
||||
const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
|
||||
const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
|
||||
@@ -288,31 +195,31 @@
|
||||
}
|
||||
aprsBarOverlay.innerHTML = html;
|
||||
aprsBarOverlay.style.display = "flex";
|
||||
}
|
||||
aprsWindow.updateAprsBar = updateAprsBar;
|
||||
aprsWindow.clearAprsBar = function() {
|
||||
}
|
||||
aprsWindow.updateAprsBar = updateAprsBar;
|
||||
aprsWindow.clearAprsBar = function() {
|
||||
resetAprsHistoryView();
|
||||
};
|
||||
aprsWindow.closeAprsBar = function() {
|
||||
};
|
||||
aprsWindow.closeAprsBar = function() {
|
||||
aprsBarDismissedAtMs = Date.now();
|
||||
if (aprsBarOverlay) {
|
||||
aprsBarOverlay.style.display = "none";
|
||||
aprsBarOverlay.innerHTML = "";
|
||||
}
|
||||
};
|
||||
function resetAprsHistoryView() {
|
||||
};
|
||||
function resetAprsHistoryView() {
|
||||
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
||||
aprsPacketHistory = [];
|
||||
updateAprsBar();
|
||||
renderAprsHistory();
|
||||
aprsWindow.clearMapMarkersByType?.("aprs");
|
||||
}
|
||||
function pruneAprsHistoryView() {
|
||||
}
|
||||
function pruneAprsHistoryView() {
|
||||
pruneAprsPacketHistory();
|
||||
updateAprsBar();
|
||||
renderAprsHistory();
|
||||
}
|
||||
function addAprsPacket(pkt) {
|
||||
}
|
||||
function addAprsPacket(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" });
|
||||
@@ -323,11 +230,11 @@
|
||||
}
|
||||
if (pkt.crcOk) scheduleAprsBarUpdate();
|
||||
scheduleAprsHistoryRender();
|
||||
}
|
||||
function normalizeServerAprsPacket(pkt) {
|
||||
}
|
||||
function normalizeServerAprsPacket(pkt) {
|
||||
return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
|
||||
}
|
||||
function onServerAprsBatch(packets) {
|
||||
}
|
||||
function onServerAprsBatch(packets) {
|
||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||
const normalized = [];
|
||||
@@ -348,8 +255,8 @@
|
||||
pruneAprsPacketHistory();
|
||||
if (hasCrcOk) scheduleAprsBarUpdate();
|
||||
scheduleAprsHistoryRender();
|
||||
}
|
||||
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
|
||||
}
|
||||
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
@@ -359,50 +266,49 @@
|
||||
console.error("APRS history clear failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
if (aprsOnlyPosBtn) {
|
||||
});
|
||||
if (aprsOnlyPosBtn) {
|
||||
aprsOnlyPosBtn.addEventListener("click", () => {
|
||||
aprsOnlyPos = !aprsOnlyPos;
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
if (aprsHideCrcBtn) {
|
||||
}
|
||||
if (aprsHideCrcBtn) {
|
||||
aprsHideCrcBtn.addEventListener("click", () => {
|
||||
aprsHideCrc = !aprsHideCrc;
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
if (aprsCollapseDupBtn) {
|
||||
}
|
||||
if (aprsCollapseDupBtn) {
|
||||
aprsCollapseDupBtn.addEventListener("click", () => {
|
||||
aprsCollapseDup = !aprsCollapseDup;
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
|
||||
}
|
||||
["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
|
||||
const btn = document.getElementById(`aprs-type-${type}`);
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", () => {
|
||||
aprsTypeFilter = type;
|
||||
renderAprsHistory();
|
||||
});
|
||||
});
|
||||
if (aprsFilterInput) {
|
||||
});
|
||||
if (aprsFilterInput) {
|
||||
aprsFilterInput.addEventListener("input", () => {
|
||||
aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
|
||||
renderAprsHistory();
|
||||
});
|
||||
}
|
||||
function onServerAprs(pkt) {
|
||||
}
|
||||
function onServerAprs(pkt) {
|
||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||
addAprsPacket(normalizeServerAprsPacket(pkt));
|
||||
}
|
||||
renderAprsHistory();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
renderAprsHistory();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
id: "aprs",
|
||||
onMessage: onServerAprs,
|
||||
onBatch: onServerAprsBatch,
|
||||
restore: onServerAprsBatch,
|
||||
reset: resetAprsHistoryView,
|
||||
prune: pruneAprsHistoryView
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
+3
-6
@@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/background-decode.ts
|
||||
var bgdWindow = window;
|
||||
(function() {
|
||||
// src/plugins/background-decode.ts
|
||||
var bgdWindow = window;
|
||||
(function() {
|
||||
"use strict";
|
||||
function bgdSupportedIds() {
|
||||
return (bgdWindow.decoderRegistry || []).filter(function(d) {
|
||||
@@ -361,5 +359,4 @@
|
||||
bgdWindow.initBackgroundDecode = initBackgroundDecode;
|
||||
bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
|
||||
bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,55 +1,53 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/bookmarks.ts
|
||||
var bridge = window;
|
||||
function bmEl(id) {
|
||||
// src/plugins/bookmarks.ts
|
||||
var bridge = window;
|
||||
function bmEl(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) throw new Error(`Missing bookmark element #${id}`);
|
||||
return element;
|
||||
}
|
||||
function errorMessage(error) {
|
||||
}
|
||||
function errorMessage(error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
var bmScope = "general";
|
||||
function bmScopeParam(prefix, scope) {
|
||||
}
|
||||
var bmScope = "general";
|
||||
function bmScopeParam(prefix, scope) {
|
||||
const sep = prefix ? "&" : "?";
|
||||
return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
|
||||
}
|
||||
var bmList = [];
|
||||
var bmOverlayList = [];
|
||||
var bmOverlayRevision = 0;
|
||||
var bmFilteredList = [];
|
||||
var bmEditScope = null;
|
||||
var bmCurrentPage = 1;
|
||||
var BM_PAGE_SIZE = 25;
|
||||
var bmSelected = /* @__PURE__ */ new Set();
|
||||
function bmFmtFreq(hz) {
|
||||
}
|
||||
var bmList = [];
|
||||
var bmOverlayList = [];
|
||||
var bmOverlayRevision = 0;
|
||||
var bmFilteredList = [];
|
||||
var bmEditScope = null;
|
||||
var bmCurrentPage = 1;
|
||||
var BM_PAGE_SIZE = 25;
|
||||
var bmSelected = /* @__PURE__ */ new Set();
|
||||
function bmFmtFreq(hz) {
|
||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||
if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + " GHz";
|
||||
if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + " MHz";
|
||||
if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + " kHz";
|
||||
return `${hz} Hz`;
|
||||
}
|
||||
function bmEsc(str) {
|
||||
}
|
||||
function bmEsc(str) {
|
||||
const d = document.createElement("div");
|
||||
d.appendChild(document.createTextNode(String(str)));
|
||||
return d.innerHTML;
|
||||
}
|
||||
function bmCanControl() {
|
||||
}
|
||||
function bmCanControl() {
|
||||
return typeof bridge.authEnabled !== "undefined" && !bridge.authEnabled || typeof bridge.authRole !== "undefined" && bridge.authRole === "control";
|
||||
}
|
||||
function bmSyncAccess() {
|
||||
}
|
||||
function bmSyncAccess() {
|
||||
const canCtrl = bmCanControl();
|
||||
const addBtn = bmEl("bm-add-btn");
|
||||
const selectAllBtn = bmEl("bm-select-all-btn");
|
||||
if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
|
||||
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
|
||||
}
|
||||
function bmListScope() {
|
||||
}
|
||||
function bmListScope() {
|
||||
const rig = typeof bridge.lastActiveRigId !== "undefined" ? bridge.lastActiveRigId : null;
|
||||
return rig || "general";
|
||||
}
|
||||
async function bmFetchOverlay() {
|
||||
}
|
||||
async function bmFetchOverlay() {
|
||||
const overlayScope = bmListScope();
|
||||
try {
|
||||
const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
|
||||
@@ -64,8 +62,8 @@
|
||||
bridge.syncBookmarkMapLocators(bmOverlayList);
|
||||
}
|
||||
if (typeof bridge.scheduleSpectrumDraw === "function") bridge.scheduleSpectrumDraw();
|
||||
}
|
||||
async function bmFetch(categoryFilter) {
|
||||
}
|
||||
async function bmFetch(categoryFilter) {
|
||||
let url = "/bookmarks";
|
||||
let hasQuery = false;
|
||||
if (categoryFilter && categoryFilter !== "") {
|
||||
@@ -88,8 +86,8 @@
|
||||
bmApplyFilters();
|
||||
void bmRefreshCategoryFilter(categoryFilter);
|
||||
await overlayPromise;
|
||||
}
|
||||
function bmApplyFilters() {
|
||||
}
|
||||
function bmApplyFilters() {
|
||||
const text = (bmEl("bm-text-filter")?.value || "").trim().toLowerCase();
|
||||
const modeFilter = (bmEl("bm-mode-filter")?.value || "").trim().toUpperCase();
|
||||
let filtered = modeFilter ? bmList.filter((bm) => (bm.mode || "").toUpperCase() === modeFilter) : bmList;
|
||||
@@ -99,8 +97,8 @@
|
||||
bmFilteredList = filtered;
|
||||
bmCurrentPage = 1;
|
||||
bmRender(filtered);
|
||||
}
|
||||
async function bmRefreshCategoryFilter(keepValue) {
|
||||
}
|
||||
async function bmRefreshCategoryFilter(keepValue) {
|
||||
const sel = bmEl("bm-category-filter");
|
||||
const modeSel = bmEl("bm-mode-filter");
|
||||
if (!sel && !modeSel) return;
|
||||
@@ -133,8 +131,8 @@
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
function bmRender(list) {
|
||||
}
|
||||
function bmRender(list) {
|
||||
const tbody = bmEl("bm-tbody");
|
||||
const emptyEl = bmEl("bm-empty");
|
||||
const paginatorEl = bmEl("bm-paginator");
|
||||
@@ -177,25 +175,25 @@
|
||||
if (pageIndicatorEl) pageIndicatorEl.textContent = `Page ${page} of ${totalPages}`;
|
||||
if (prevBtn) prevBtn.disabled = page <= 1;
|
||||
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
||||
}
|
||||
function bmChangePage(delta) {
|
||||
}
|
||||
function bmChangePage(delta) {
|
||||
const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
|
||||
const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
|
||||
if (nextPage === bmCurrentPage) return;
|
||||
bmCurrentPage = nextPage;
|
||||
bmRender(bmFilteredList);
|
||||
}
|
||||
function bmReadDecoders() {
|
||||
}
|
||||
function bmReadDecoders() {
|
||||
return (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).filter((d) => bmEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
|
||||
}
|
||||
function bmWriteDecoders(decoders) {
|
||||
}
|
||||
function bmWriteDecoders(decoders) {
|
||||
const set = new Set(decoders || []);
|
||||
(bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
|
||||
const el = bmEl("bm-dec-" + d.id);
|
||||
if (el) el.checked = set.has(d.id);
|
||||
});
|
||||
}
|
||||
function bmBuildDecoderCheckboxes() {
|
||||
}
|
||||
function bmBuildDecoderCheckboxes() {
|
||||
const container = bmEl("bm-decoder-checkboxes");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
@@ -205,8 +203,8 @@
|
||||
label.innerHTML = '<input type="checkbox" id="bm-dec-' + d.id + '" value="' + d.id + '" /> ' + d.label;
|
||||
container.appendChild(label);
|
||||
});
|
||||
}
|
||||
function bmOpenForm(bm) {
|
||||
}
|
||||
function bmOpenForm(bm) {
|
||||
const wrap = bmEl("bm-form-wrap");
|
||||
if (!wrap) return;
|
||||
bmEditScope = bm ? bm.scope || bmScope : null;
|
||||
@@ -223,12 +221,12 @@
|
||||
bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
|
||||
wrap.style.display = "flex";
|
||||
bmEl("bm-name").focus();
|
||||
}
|
||||
function bmCloseForm() {
|
||||
}
|
||||
function bmCloseForm() {
|
||||
const wrap = bmEl("bm-form-wrap");
|
||||
if (wrap) wrap.style.display = "none";
|
||||
}
|
||||
function bmPrefillFromStatus() {
|
||||
}
|
||||
function bmPrefillFromStatus() {
|
||||
if (typeof bridge.lastFreqHz === "number" && Number.isFinite(bridge.lastFreqHz)) {
|
||||
bmEl("bm-freq").value = String(Math.round(bridge.lastFreqHz));
|
||||
}
|
||||
@@ -243,8 +241,8 @@
|
||||
return btn && btn.dataset.enabled === "true";
|
||||
}).map((d) => d.id);
|
||||
bmWriteDecoders(activeDecoders);
|
||||
}
|
||||
async function bmSave(e) {
|
||||
}
|
||||
async function bmSave(e) {
|
||||
e.preventDefault();
|
||||
const id = bmEl("bm-id").value;
|
||||
const name = bmEl("bm-name").value.trim();
|
||||
@@ -304,8 +302,8 @@
|
||||
if (formError) formError.textContent = "Failed to save bookmark: " + errorMessage(err);
|
||||
bridge.trxUi.notify?.("Bookmark could not be saved", { kind: "error" });
|
||||
}
|
||||
}
|
||||
async function bmDelete(id) {
|
||||
}
|
||||
async function bmDelete(id) {
|
||||
if (!await bridge.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
|
||||
const bm = bmList.find((b) => b.id === id);
|
||||
const scope = bm ? bm.scope : void 0;
|
||||
@@ -319,8 +317,8 @@
|
||||
console.error("Failed to delete bookmark:", err);
|
||||
bridge.trxUi.notify?.("Failed to delete bookmark: " + errorMessage(err), { kind: "error" });
|
||||
}
|
||||
}
|
||||
function bmApply(bm) {
|
||||
}
|
||||
function bmApply(bm) {
|
||||
try {
|
||||
if (typeof bridge.modeEl !== "undefined" && bridge.modeEl) {
|
||||
bridge.modeEl.value = (bm.mode || "").toUpperCase();
|
||||
@@ -402,10 +400,10 @@
|
||||
} catch (err) {
|
||||
console.error("Failed to apply bookmark:", err);
|
||||
}
|
||||
}
|
||||
bridge.trx ??= {};
|
||||
bridge.trx.modules ??= {};
|
||||
bridge.trx.modules.bookmarks = {
|
||||
}
|
||||
bridge.trx ??= {};
|
||||
bridge.trx.modules ??= {};
|
||||
bridge.trx.modules.bookmarks = {
|
||||
get overlayList() {
|
||||
return bmOverlayList;
|
||||
},
|
||||
@@ -417,8 +415,8 @@
|
||||
bmOverlayRevision += 1;
|
||||
},
|
||||
apply: bmApply
|
||||
};
|
||||
function bmUpdateSelectionUi() {
|
||||
};
|
||||
function bmUpdateSelectionUi() {
|
||||
const count = bmSelected.size;
|
||||
const canCtrl = bmCanControl();
|
||||
const visible = count > 0 && canCtrl;
|
||||
@@ -436,8 +434,8 @@
|
||||
const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
|
||||
selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
|
||||
}
|
||||
}
|
||||
function bmPopulateMoveTarget() {
|
||||
}
|
||||
function bmPopulateMoveTarget() {
|
||||
const sel = bmEl("bm-move-target");
|
||||
if (!sel) return;
|
||||
const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
|
||||
@@ -460,8 +458,8 @@
|
||||
if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) {
|
||||
sel.value = prev;
|
||||
}
|
||||
}
|
||||
async function bmMoveSelected() {
|
||||
}
|
||||
async function bmMoveSelected() {
|
||||
const ids = Array.from(bmSelected);
|
||||
if (ids.length === 0) return;
|
||||
const target = bmEl("bm-move-target")?.value;
|
||||
@@ -497,8 +495,8 @@
|
||||
console.error("Failed to move bookmarks:", err);
|
||||
bridge.trxUi.notify?.("Failed to move bookmarks: " + errorMessage(err), { kind: "error" });
|
||||
}
|
||||
}
|
||||
function bmSyncSelectAllCheckbox() {
|
||||
}
|
||||
function bmSyncSelectAllCheckbox() {
|
||||
const selectAll = bmEl("bm-select-all");
|
||||
if (!selectAll) return;
|
||||
const checkboxes = document.querySelectorAll(".bm-row-sel");
|
||||
@@ -510,8 +508,8 @@
|
||||
const checkedCount = Array.from(checkboxes).filter((cb) => cb.checked).length;
|
||||
selectAll.checked = checkedCount === checkboxes.length;
|
||||
selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
|
||||
}
|
||||
async function bmDeleteSelected() {
|
||||
}
|
||||
async function bmDeleteSelected() {
|
||||
const ids = Array.from(bmSelected);
|
||||
if (ids.length === 0) return;
|
||||
if (!await bridge.trxUi.confirm({
|
||||
@@ -542,8 +540,8 @@
|
||||
console.error("Failed to delete bookmarks:", err);
|
||||
bridge.trxUi.notify?.("Failed to delete bookmarks: " + errorMessage(err), { kind: "error" });
|
||||
}
|
||||
}
|
||||
function bmPopulateScopePicker() {
|
||||
}
|
||||
function bmPopulateScopePicker() {
|
||||
const picker = bmEl("bm-scope-picker");
|
||||
if (!picker) return;
|
||||
const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
|
||||
@@ -562,8 +560,8 @@
|
||||
picker.value = "general";
|
||||
}
|
||||
bmScope = picker.value;
|
||||
}
|
||||
(function initBookmarks() {
|
||||
}
|
||||
(function initBookmarks() {
|
||||
bmSyncAccess();
|
||||
bmBuildDecoderCheckboxes();
|
||||
if (typeof bridge.onDecoderRegistryReady === "function") {
|
||||
@@ -675,5 +673,4 @@
|
||||
})();
|
||||
});
|
||||
void bmFetch("");
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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)) : `<span class="aprs-byte">0x${byte.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
||||
}
|
||||
function renderAprsCharacter(character) {
|
||||
const code = character.charCodeAt(0);
|
||||
return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `<span class="aprs-byte">0x${code.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
||||
}
|
||||
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 `<span class="aprs-symbol aprs-symbol-local" title="${table} APRS symbol ${symbol}">${symbol}</span>`;
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
aprsPacketCategory,
|
||||
aprsCategoryLabel,
|
||||
aprsAgeText,
|
||||
collapseAprsDuplicates,
|
||||
aprsHexBytes,
|
||||
renderAprsInfo,
|
||||
renderLocalAprsSymbol,
|
||||
normalizeAprsPacket
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
// src/plugins/ftx-family.ts
|
||||
var bridge = window;
|
||||
function finiteNumber(value) {
|
||||
const number = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
function isAlphaNumeric(value) {
|
||||
return value !== void 0 && /[A-Za-z0-9]/.test(value);
|
||||
}
|
||||
function isGrid(value) {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
|
||||
}
|
||||
function escapeFtxHtml(input) {
|
||||
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
function extractFtxGrids(message) {
|
||||
return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
|
||||
}
|
||||
function tokenize(message) {
|
||||
return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
|
||||
}
|
||||
function isCallsign(token) {
|
||||
return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
|
||||
}
|
||||
function extractFtxLocatorDetails(message) {
|
||||
const tokens = tokenize(message);
|
||||
const grids = extractFtxGrids(message);
|
||||
const gridIndex = tokens.findIndex(isGrid);
|
||||
const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
|
||||
const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
|
||||
const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
|
||||
const target = directed ? callsigns[0] ?? null : null;
|
||||
return grids.map((grid) => ({ grid, station: source, source, target }));
|
||||
}
|
||||
function extractFtxCallsign(message) {
|
||||
return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
|
||||
}
|
||||
function renderFtxMessage(message) {
|
||||
let html = "";
|
||||
let index = 0;
|
||||
while (index < message.length) {
|
||||
if (!isAlphaNumeric(message[index])) {
|
||||
html += escapeFtxHtml(message[index] ?? "");
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let end = index + 1;
|
||||
while (end < message.length && isAlphaNumeric(message[end])) end += 1;
|
||||
const token = message.slice(index, end);
|
||||
const grid = token.toUpperCase();
|
||||
html += isGrid(grid) ? `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>` : escapeFtxHtml(token);
|
||||
index = end;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
function installFtxCompatibilityHelpers() {
|
||||
bridge.renderFt8Message = renderFtxMessage;
|
||||
bridge.ft8EscapeHtml = escapeFtxHtml;
|
||||
bridge.ft8ExtractLocatorDetails = extractFtxLocatorDetails;
|
||||
bridge.ft8ExtractAllGrids = extractFtxGrids;
|
||||
bridge.ft8ExtractLikelyCallsign = extractFtxCallsign;
|
||||
}
|
||||
function initializeFt8FamilyBar() {
|
||||
const labels = { ft8: "FT8", ft4: "FT4", ft2: "FT2" };
|
||||
const builders = {};
|
||||
const dismissed = { ft8: 0, ft4: 0, ft2: 0 };
|
||||
const overlay = document.getElementById("ft8-bar-overlay");
|
||||
let active = "ft8";
|
||||
const update = () => {
|
||||
if (!overlay) return;
|
||||
const mode = (document.getElementById("mode")?.value ?? "").toUpperCase();
|
||||
const result = builders[active]?.();
|
||||
if (mode !== "DIG" && mode !== "USB" || !result || result.count === 0 || result.newestTsMs <= dismissed[active]) {
|
||||
overlay.style.display = "none";
|
||||
overlay.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const label = labels[active];
|
||||
overlay.innerHTML = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">${label}</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-actions"><span class="aprs-bar-window">Last 15 minutes</span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearFt8Bar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearFt8Bar();}" aria-label="Clear ${label} overlay">Clear</span></span><button class="aprs-bar-close" type="button" onclick="window.closeFt8Bar()" aria-label="Close ${label} overlay">×</button></span></div>${result.html}`;
|
||||
overlay.style.display = "flex";
|
||||
};
|
||||
bridge.registerFt8FamilyBarRenderer = (decoder, builder) => {
|
||||
builders[decoder] = builder;
|
||||
};
|
||||
bridge.setFt8FamilyBarDecoder = (decoder) => {
|
||||
active = decoder;
|
||||
update();
|
||||
};
|
||||
bridge.updateFt8Bar = update;
|
||||
bridge.clearFt8Bar = () => {
|
||||
bridge.trxPluginRuntime.reset(active);
|
||||
};
|
||||
bridge.closeFt8Bar = () => {
|
||||
dismissed[active] = Date.now();
|
||||
update();
|
||||
};
|
||||
}
|
||||
function initializeFtxDecoder(config) {
|
||||
const { id, label, periodMs, periodDigits = 1 } = config;
|
||||
const status = document.getElementById(`${id}-status`);
|
||||
const period = document.getElementById(`${id}-period`);
|
||||
const messagesElement = document.getElementById(`${id}-messages`);
|
||||
const filterInput = document.getElementById(`${id}-filter`);
|
||||
let filterText = "";
|
||||
let history = [];
|
||||
const renderMessage = (message) => {
|
||||
return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
|
||||
};
|
||||
const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
|
||||
const prune = () => {
|
||||
const cutoff = Date.now() - retentionMs();
|
||||
history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
|
||||
};
|
||||
const schedule = (job) => {
|
||||
if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
|
||||
else job();
|
||||
};
|
||||
const displayFrequency = (value) => {
|
||||
const raw = finiteNumber(value);
|
||||
if (raw === null) return null;
|
||||
const base = finiteNumber(bridge.ft8BaseHz);
|
||||
return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
|
||||
};
|
||||
const renderRow = (message) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
const raw = message.message ?? "";
|
||||
row.dataset.message = raw.toUpperCase();
|
||||
row.dataset.decoder = id;
|
||||
const storedFrequency = finiteNumber(message.freq_hz);
|
||||
row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
row.innerHTML = `<span class="ft8-time">${time}</span><span class="ft8-snr">${snr?.toFixed(1) ?? "--"}</span><span class="ft8-dt">${delta?.toFixed(2) ?? "--"}</span><span class="ft8-freq">${frequency?.toFixed(0) ?? "--"}</span><span class="ft8-msg">${renderMessage(raw)}</span>`;
|
||||
return row;
|
||||
};
|
||||
const render = () => {
|
||||
prune();
|
||||
if (!messagesElement) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let count = 0;
|
||||
for (const message of history) {
|
||||
if (count >= 200) break;
|
||||
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
||||
fragment.appendChild(renderRow(message));
|
||||
count += 1;
|
||||
}
|
||||
messagesElement.replaceChildren(fragment);
|
||||
};
|
||||
const normalize = (message) => {
|
||||
const raw = message.message ?? "";
|
||||
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
||||
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
||||
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
if (grids.length > 0) {
|
||||
bridge.mapAddLocator?.(raw, grids, id, station, {
|
||||
...message,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
locator_details: locatorDetails
|
||||
});
|
||||
}
|
||||
return {
|
||||
receiver: bridge.getDecodeRigMeta?.() ?? null,
|
||||
ts_ms: message.ts_ms,
|
||||
snr_db: message.snr_db,
|
||||
dt_s: message.dt_s,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
message: message.message,
|
||||
_tsMs: finiteNumber(message.ts_ms) ?? Date.now()
|
||||
};
|
||||
};
|
||||
const receiveBatch = (messages) => {
|
||||
if (messages.length === 0) return;
|
||||
if (status) status.textContent = "Receiving";
|
||||
history = messages.map(normalize).reverse().concat(history);
|
||||
prune();
|
||||
bridge.setFt8FamilyBarDecoder?.(id);
|
||||
bridge.updateFt8Bar?.();
|
||||
schedule(render);
|
||||
};
|
||||
const reset = () => {
|
||||
history = [];
|
||||
bridge.updateFt8Bar?.();
|
||||
render();
|
||||
bridge.clearMapMarkersByType?.(id);
|
||||
};
|
||||
const barFrames = () => {
|
||||
const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
|
||||
let html = "";
|
||||
for (const message of recent) {
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "" : `<span class="aprs-bar-time">${bridge.fmtTime?.(timestamp) ?? ""}</span>`;
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
|
||||
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${time}<span class="aprs-bar-call">${renderMessage(message.message ?? "")}</span>${detail ? ` · ${detail}` : ""}</div></div>`;
|
||||
}
|
||||
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
|
||||
};
|
||||
bridge.trxPluginRuntime.registerDecoder({
|
||||
id,
|
||||
onMessage: (message) => {
|
||||
receiveBatch([message]);
|
||||
},
|
||||
onBatch: receiveBatch,
|
||||
restore: receiveBatch,
|
||||
prune: () => {
|
||||
prune();
|
||||
render();
|
||||
},
|
||||
reset
|
||||
});
|
||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||
const updatePeriod = () => {
|
||||
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
|
||||
};
|
||||
updatePeriod();
|
||||
window.setInterval(updatePeriod, 250);
|
||||
filterInput?.addEventListener("input", () => {
|
||||
filterText = filterInput.value.trim().toUpperCase();
|
||||
render();
|
||||
});
|
||||
messagesElement?.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof Element)) return;
|
||||
const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
|
||||
if (grid) {
|
||||
bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const toggle = document.getElementById(`${id}-decode-toggle-btn`);
|
||||
toggle?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
|
||||
await bridge.postPath?.(`/toggle_${id}_decode`);
|
||||
} catch (error) {
|
||||
console.error(`${label} toggle failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
await bridge.postPath?.(`/clear_${id}_decode`);
|
||||
reset();
|
||||
} catch (error) {
|
||||
console.error(`${label} history clear failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
installFtxCompatibilityHelpers,
|
||||
initializeFt8FamilyBar,
|
||||
initializeFtxDecoder
|
||||
};
|
||||
@@ -1,35 +1,33 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/cw.ts
|
||||
var cwWindow = window;
|
||||
var cwStatusEl = document.getElementById("cw-status");
|
||||
var cwOutputEl = document.getElementById("cw-output");
|
||||
var cwAutoInput = document.getElementById("cw-auto");
|
||||
var cwWpmInput = document.getElementById("cw-wpm");
|
||||
var cwToneInput = document.getElementById("cw-tone");
|
||||
var cwSignalIndicator = document.getElementById("cw-signal-indicator");
|
||||
var cwToneCanvas = document.getElementById("cw-tone-waterfall");
|
||||
var cwToneGl = cwToneCanvas && cwWindow.createTrxWebGlRenderer ? cwWindow.createTrxWebGlRenderer(cwToneCanvas, { alpha: true }) : null;
|
||||
var cwTonePickerEl = document.querySelector(".cw-tone-picker");
|
||||
var cwToneRangeEl = document.getElementById("cw-tone-range");
|
||||
var cwBarOverlay = document.getElementById("cw-bar-overlay");
|
||||
var CW_MAX_LINES = 200;
|
||||
var CW_TONE_MIN_HZ = 100;
|
||||
var CW_TONE_MAX_HZ = 1e4;
|
||||
var CW_WPM_MIN = 5;
|
||||
var CW_WPM_MAX = 40;
|
||||
var CW_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var CW_BAR_LINE_GAP_MS = 5e3;
|
||||
var cwLastAppendTime = 0;
|
||||
var cwTonePickerRaf = null;
|
||||
var cwBarHistory = [];
|
||||
var cwBarCurrentLine = null;
|
||||
var cwBarDismissedAtMs = 0;
|
||||
var cwAutoLocalOverride = null;
|
||||
function escapeCwHtml(input) {
|
||||
// src/plugins/cw.ts
|
||||
var cwWindow = window;
|
||||
var cwStatusEl = document.getElementById("cw-status");
|
||||
var cwOutputEl = document.getElementById("cw-output");
|
||||
var cwAutoInput = document.getElementById("cw-auto");
|
||||
var cwWpmInput = document.getElementById("cw-wpm");
|
||||
var cwToneInput = document.getElementById("cw-tone");
|
||||
var cwSignalIndicator = document.getElementById("cw-signal-indicator");
|
||||
var cwToneCanvas = document.getElementById("cw-tone-waterfall");
|
||||
var cwToneGl = cwToneCanvas && cwWindow.createTrxWebGlRenderer ? cwWindow.createTrxWebGlRenderer(cwToneCanvas, { alpha: true }) : null;
|
||||
var cwTonePickerEl = document.querySelector(".cw-tone-picker");
|
||||
var cwToneRangeEl = document.getElementById("cw-tone-range");
|
||||
var cwBarOverlay = document.getElementById("cw-bar-overlay");
|
||||
var CW_MAX_LINES = 200;
|
||||
var CW_TONE_MIN_HZ = 100;
|
||||
var CW_TONE_MAX_HZ = 1e4;
|
||||
var CW_WPM_MIN = 5;
|
||||
var CW_WPM_MAX = 40;
|
||||
var CW_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var CW_BAR_LINE_GAP_MS = 5e3;
|
||||
var cwLastAppendTime = 0;
|
||||
var cwTonePickerRaf = null;
|
||||
var cwBarHistory = [];
|
||||
var cwBarCurrentLine = null;
|
||||
var cwBarDismissedAtMs = 0;
|
||||
var cwAutoLocalOverride = null;
|
||||
function escapeCwHtml(input) {
|
||||
return cwWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
function applyCwAutoUi(enabled) {
|
||||
}
|
||||
function applyCwAutoUi(enabled) {
|
||||
if (cwAutoInput) cwAutoInput.checked = enabled;
|
||||
if (cwWpmInput) {
|
||||
cwWpmInput.disabled = enabled;
|
||||
@@ -42,20 +40,20 @@
|
||||
if (cwTonePickerEl) {
|
||||
cwTonePickerEl.classList.toggle("is-auto", enabled);
|
||||
}
|
||||
}
|
||||
cwWindow.applyCwAutoUi = applyCwAutoUi;
|
||||
cwWindow.applyCwAutoUiFromServer = function(enabled) {
|
||||
}
|
||||
cwWindow.applyCwAutoUi = applyCwAutoUi;
|
||||
cwWindow.applyCwAutoUiFromServer = function(enabled) {
|
||||
if (cwAutoLocalOverride !== null) return;
|
||||
applyCwAutoUi(enabled);
|
||||
};
|
||||
function cwBarFlushCurrentLine() {
|
||||
};
|
||||
function cwBarFlushCurrentLine() {
|
||||
if (cwBarCurrentLine && cwBarCurrentLine.text.trim()) {
|
||||
cwBarHistory.unshift(cwBarCurrentLine);
|
||||
if (cwBarHistory.length > 50) cwBarHistory.length = 50;
|
||||
}
|
||||
cwBarCurrentLine = null;
|
||||
}
|
||||
function updateCwBar() {
|
||||
}
|
||||
function updateCwBar() {
|
||||
if (!cwBarOverlay) return;
|
||||
const mode = (document.getElementById("mode")?.value || "").toUpperCase();
|
||||
const isCw = mode === "CW" || mode === "CWR";
|
||||
@@ -79,29 +77,29 @@
|
||||
}
|
||||
cwBarOverlay.innerHTML = html;
|
||||
cwBarOverlay.style.display = "flex";
|
||||
}
|
||||
cwWindow.updateCwBar = updateCwBar;
|
||||
cwWindow.clearCwBar = function() {
|
||||
}
|
||||
cwWindow.updateCwBar = updateCwBar;
|
||||
cwWindow.clearCwBar = function() {
|
||||
resetCwHistoryView();
|
||||
};
|
||||
cwWindow.closeCwBar = function() {
|
||||
};
|
||||
cwWindow.closeCwBar = function() {
|
||||
cwBarDismissedAtMs = Date.now();
|
||||
if (cwBarOverlay) {
|
||||
cwBarOverlay.style.display = "none";
|
||||
cwBarOverlay.innerHTML = "";
|
||||
}
|
||||
};
|
||||
function clampCwWpm(wpm) {
|
||||
};
|
||||
function clampCwWpm(wpm) {
|
||||
const numeric = Number(wpm);
|
||||
if (!Number.isFinite(numeric)) return 15;
|
||||
return Math.round(Math.max(CW_WPM_MIN, Math.min(CW_WPM_MAX, numeric)));
|
||||
}
|
||||
function clampCwTone(tone) {
|
||||
}
|
||||
function clampCwTone(tone) {
|
||||
const numeric = Number(tone);
|
||||
if (!Number.isFinite(numeric)) return 700;
|
||||
return Math.round(Math.max(CW_TONE_MIN_HZ, Math.min(CW_TONE_MAX_HZ, numeric)));
|
||||
}
|
||||
function currentCwToneRange() {
|
||||
}
|
||||
function currentCwToneRange() {
|
||||
const tunedHz = Number.isFinite(cwWindow.lastFreqHz) ? Number(cwWindow.lastFreqHz) : NaN;
|
||||
const bandwidthHz = Number.isFinite(cwWindow.currentBandwidthHz) ? Number(cwWindow.currentBandwidthHz) : NaN;
|
||||
if (!Number.isFinite(tunedHz) || !Number.isFinite(bandwidthHz) || bandwidthHz <= 0) {
|
||||
@@ -122,17 +120,17 @@
|
||||
lowerSideband,
|
||||
mode
|
||||
};
|
||||
}
|
||||
function cwToneToRfHz(range, toneHz) {
|
||||
}
|
||||
function cwToneToRfHz(range, toneHz) {
|
||||
if (!range) return NaN;
|
||||
return range.lowerSideband ? range.tunedHz - toneHz : range.tunedHz + toneHz;
|
||||
}
|
||||
function toneClampForRange(tone, range) {
|
||||
}
|
||||
function toneClampForRange(tone, range) {
|
||||
const clamped = clampCwTone(tone);
|
||||
if (!range) return clamped;
|
||||
return Math.max(range.toneMinHz, Math.min(range.toneMaxHz, clamped));
|
||||
}
|
||||
function ensureCwToneCanvasResolution() {
|
||||
}
|
||||
function ensureCwToneCanvasResolution() {
|
||||
if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return false;
|
||||
const rect = cwToneCanvas.getBoundingClientRect();
|
||||
const cssWidth = Math.round(rect.width);
|
||||
@@ -142,8 +140,8 @@
|
||||
}
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
return cwToneGl.ensureSize(cssWidth, cssHeight, dpr);
|
||||
}
|
||||
function drawCwTonePicker() {
|
||||
}
|
||||
function drawCwTonePicker() {
|
||||
if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return;
|
||||
ensureCwToneCanvasResolution();
|
||||
if (cwToneCanvas.width < 8 || cwToneCanvas.height < 8) return;
|
||||
@@ -240,8 +238,8 @@
|
||||
if (cwAutoInput?.checked) {
|
||||
cwToneGl.fillRect(0, 0, width, height, [0, 0, 0, 0.22]);
|
||||
}
|
||||
}
|
||||
async function setCwTone(tone, { syncInput = true } = {}) {
|
||||
}
|
||||
async function setCwTone(tone, { syncInput = true } = {}) {
|
||||
const range = currentCwToneRange();
|
||||
const clamped = toneClampForRange(tone, range);
|
||||
if (cwToneInput && syncInput) {
|
||||
@@ -253,8 +251,8 @@
|
||||
console.error("CW tone set failed", e);
|
||||
}
|
||||
drawCwTonePicker();
|
||||
}
|
||||
if (cwAutoInput) {
|
||||
}
|
||||
if (cwAutoInput) {
|
||||
cwAutoInput.addEventListener("change", () => {
|
||||
void (async () => {
|
||||
const enabled = cwAutoInput.checked;
|
||||
@@ -270,8 +268,8 @@
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
if (cwWpmInput) {
|
||||
}
|
||||
if (cwWpmInput) {
|
||||
cwWpmInput.addEventListener("change", () => {
|
||||
void (async () => {
|
||||
if (cwAutoInput?.checked) return;
|
||||
@@ -284,13 +282,13 @@
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
if (cwToneInput) {
|
||||
}
|
||||
if (cwToneInput) {
|
||||
cwToneInput.addEventListener("change", () => {
|
||||
if (!cwAutoInput?.checked) void setCwTone(cwToneInput.value);
|
||||
});
|
||||
}
|
||||
if (cwToneCanvas) {
|
||||
}
|
||||
if (cwToneCanvas) {
|
||||
cwToneCanvas.addEventListener("click", (event) => {
|
||||
if (cwAutoInput?.checked) return;
|
||||
const rect = cwToneCanvas.getBoundingClientRect();
|
||||
@@ -301,16 +299,16 @@
|
||||
const tone = range.toneMinHz + frac * range.toneSpanHz;
|
||||
void setCwTone(tone);
|
||||
});
|
||||
}
|
||||
function resetCwHistoryView() {
|
||||
}
|
||||
function resetCwHistoryView() {
|
||||
if (cwOutputEl) cwOutputEl.innerHTML = "";
|
||||
cwLastAppendTime = 0;
|
||||
cwBarHistory = [];
|
||||
cwBarCurrentLine = null;
|
||||
updateCwBar();
|
||||
drawCwTonePicker();
|
||||
}
|
||||
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
|
||||
}
|
||||
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
@@ -320,8 +318,8 @@
|
||||
console.error("CW history clear failed", error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
function onServerCw(evt) {
|
||||
});
|
||||
function onServerCw(evt) {
|
||||
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
|
||||
if (evt.text && cwOutputEl) {
|
||||
const now = Date.now();
|
||||
@@ -375,29 +373,28 @@
|
||||
cwTonePickerRaf = null;
|
||||
drawCwTonePicker();
|
||||
});
|
||||
}
|
||||
function restoreCwHistory(events) {
|
||||
}
|
||||
function restoreCwHistory(events) {
|
||||
if (!Array.isArray(events) || events.length === 0) return;
|
||||
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
|
||||
for (const evt of events) {
|
||||
onServerCw(evt);
|
||||
}
|
||||
}
|
||||
cwWindow.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
cwWindow.trxPluginRuntime.registerDecoder({
|
||||
id: "cw",
|
||||
onMessage: onServerCw,
|
||||
restore: restoreCwHistory,
|
||||
reset: resetCwHistoryView
|
||||
});
|
||||
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
||||
});
|
||||
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
||||
ensureCwToneCanvasResolution();
|
||||
drawCwTonePicker();
|
||||
};
|
||||
window.addEventListener("resize", () => {
|
||||
};
|
||||
window.addEventListener("resize", () => {
|
||||
if (ensureCwToneCanvasResolution()) drawCwTonePicker();
|
||||
});
|
||||
applyCwAutoUi(!!cwAutoInput?.checked);
|
||||
updateCwBar();
|
||||
ensureCwToneCanvasResolution();
|
||||
drawCwTonePicker();
|
||||
})();
|
||||
});
|
||||
applyCwAutoUi(!!cwAutoInput?.checked);
|
||||
updateCwBar();
|
||||
ensureCwToneCanvasResolution();
|
||||
drawCwTonePicker();
|
||||
|
||||
@@ -1,227 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/ftx-family.ts
|
||||
var bridge = window;
|
||||
function finiteNumber(value) {
|
||||
const number = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
function isAlphaNumeric(value) {
|
||||
return value !== void 0 && /[A-Za-z0-9]/.test(value);
|
||||
}
|
||||
function isGrid(value) {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
|
||||
}
|
||||
function escapeFtxHtml(input) {
|
||||
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
function extractFtxGrids(message) {
|
||||
return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
|
||||
}
|
||||
function tokenize(message) {
|
||||
return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
|
||||
}
|
||||
function isCallsign(token) {
|
||||
return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
|
||||
}
|
||||
function extractFtxLocatorDetails(message) {
|
||||
const tokens = tokenize(message);
|
||||
const grids = extractFtxGrids(message);
|
||||
const gridIndex = tokens.findIndex(isGrid);
|
||||
const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
|
||||
const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
|
||||
const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
|
||||
const target = directed ? callsigns[0] ?? null : null;
|
||||
return grids.map((grid) => ({ grid, station: source, source, target }));
|
||||
}
|
||||
function extractFtxCallsign(message) {
|
||||
return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
|
||||
}
|
||||
function renderFtxMessage(message) {
|
||||
let html = "";
|
||||
let index = 0;
|
||||
while (index < message.length) {
|
||||
if (!isAlphaNumeric(message[index])) {
|
||||
html += escapeFtxHtml(message[index] ?? "");
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let end = index + 1;
|
||||
while (end < message.length && isAlphaNumeric(message[end])) end += 1;
|
||||
const token = message.slice(index, end);
|
||||
const grid = token.toUpperCase();
|
||||
html += isGrid(grid) ? `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>` : escapeFtxHtml(token);
|
||||
index = end;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
function initializeFtxDecoder(config) {
|
||||
const { id, label, periodMs, periodDigits = 1 } = config;
|
||||
const status = document.getElementById(`${id}-status`);
|
||||
const period = document.getElementById(`${id}-period`);
|
||||
const messagesElement = document.getElementById(`${id}-messages`);
|
||||
const filterInput = document.getElementById(`${id}-filter`);
|
||||
let filterText = "";
|
||||
let history = [];
|
||||
const renderMessage = (message) => {
|
||||
return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
|
||||
};
|
||||
const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
|
||||
const prune = () => {
|
||||
const cutoff = Date.now() - retentionMs();
|
||||
history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
|
||||
};
|
||||
const schedule = (job) => {
|
||||
if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
|
||||
else job();
|
||||
};
|
||||
const displayFrequency = (value) => {
|
||||
const raw = finiteNumber(value);
|
||||
if (raw === null) return null;
|
||||
const base = finiteNumber(bridge.ft8BaseHz);
|
||||
return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
|
||||
};
|
||||
const renderRow = (message) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
const raw = message.message ?? "";
|
||||
row.dataset.message = raw.toUpperCase();
|
||||
row.dataset.decoder = id;
|
||||
const storedFrequency = finiteNumber(message.freq_hz);
|
||||
row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
row.innerHTML = `<span class="ft8-time">${time}</span><span class="ft8-snr">${snr?.toFixed(1) ?? "--"}</span><span class="ft8-dt">${delta?.toFixed(2) ?? "--"}</span><span class="ft8-freq">${frequency?.toFixed(0) ?? "--"}</span><span class="ft8-msg">${renderMessage(raw)}</span>`;
|
||||
return row;
|
||||
};
|
||||
const render = () => {
|
||||
prune();
|
||||
if (!messagesElement) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let count = 0;
|
||||
for (const message of history) {
|
||||
if (count >= 200) break;
|
||||
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
||||
fragment.appendChild(renderRow(message));
|
||||
count += 1;
|
||||
}
|
||||
messagesElement.replaceChildren(fragment);
|
||||
};
|
||||
const normalize = (message) => {
|
||||
const raw = message.message ?? "";
|
||||
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
||||
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
||||
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
if (grids.length > 0) {
|
||||
bridge.mapAddLocator?.(raw, grids, id, station, {
|
||||
...message,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
locator_details: locatorDetails
|
||||
});
|
||||
}
|
||||
return {
|
||||
receiver: bridge.getDecodeRigMeta?.() ?? null,
|
||||
ts_ms: message.ts_ms,
|
||||
snr_db: message.snr_db,
|
||||
dt_s: message.dt_s,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
message: message.message,
|
||||
_tsMs: finiteNumber(message.ts_ms) ?? Date.now()
|
||||
};
|
||||
};
|
||||
const receiveBatch = (messages) => {
|
||||
if (messages.length === 0) return;
|
||||
if (status) status.textContent = "Receiving";
|
||||
history = messages.map(normalize).reverse().concat(history);
|
||||
prune();
|
||||
bridge.setFt8FamilyBarDecoder?.(id);
|
||||
bridge.updateFt8Bar?.();
|
||||
schedule(render);
|
||||
};
|
||||
const reset = () => {
|
||||
history = [];
|
||||
bridge.updateFt8Bar?.();
|
||||
render();
|
||||
bridge.clearMapMarkersByType?.(id);
|
||||
};
|
||||
const barFrames = () => {
|
||||
const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
|
||||
let html = "";
|
||||
for (const message of recent) {
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "" : `<span class="aprs-bar-time">${bridge.fmtTime?.(timestamp) ?? ""}</span>`;
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
|
||||
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${time}<span class="aprs-bar-call">${renderMessage(message.message ?? "")}</span>${detail ? ` · ${detail}` : ""}</div></div>`;
|
||||
}
|
||||
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
|
||||
};
|
||||
bridge.trxPluginRuntime.registerDecoder({
|
||||
id,
|
||||
onMessage: (message) => {
|
||||
receiveBatch([message]);
|
||||
},
|
||||
onBatch: receiveBatch,
|
||||
restore: receiveBatch,
|
||||
prune: () => {
|
||||
prune();
|
||||
render();
|
||||
},
|
||||
reset
|
||||
});
|
||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||
const updatePeriod = () => {
|
||||
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
|
||||
};
|
||||
updatePeriod();
|
||||
window.setInterval(updatePeriod, 250);
|
||||
filterInput?.addEventListener("input", () => {
|
||||
filterText = filterInput.value.trim().toUpperCase();
|
||||
render();
|
||||
});
|
||||
messagesElement?.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof Element)) return;
|
||||
const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
|
||||
if (grid) {
|
||||
bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const toggle = document.getElementById(`${id}-decode-toggle-btn`);
|
||||
toggle?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
|
||||
await bridge.postPath?.(`/toggle_${id}_decode`);
|
||||
} catch (error) {
|
||||
console.error(`${label} toggle failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
await bridge.postPath?.(`/clear_${id}_decode`);
|
||||
reset();
|
||||
} catch (error) {
|
||||
console.error(`${label} history clear failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
import {
|
||||
initializeFtxDecoder
|
||||
} from "./chunk-SGMG5LG2.js";
|
||||
|
||||
// src/plugins/ft2.ts
|
||||
initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
|
||||
})();
|
||||
// src/plugins/ft2.ts
|
||||
initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
|
||||
|
||||
@@ -1,227 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/ftx-family.ts
|
||||
var bridge = window;
|
||||
function finiteNumber(value) {
|
||||
const number = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
function isAlphaNumeric(value) {
|
||||
return value !== void 0 && /[A-Za-z0-9]/.test(value);
|
||||
}
|
||||
function isGrid(value) {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
|
||||
}
|
||||
function escapeFtxHtml(input) {
|
||||
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
function extractFtxGrids(message) {
|
||||
return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
|
||||
}
|
||||
function tokenize(message) {
|
||||
return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
|
||||
}
|
||||
function isCallsign(token) {
|
||||
return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
|
||||
}
|
||||
function extractFtxLocatorDetails(message) {
|
||||
const tokens = tokenize(message);
|
||||
const grids = extractFtxGrids(message);
|
||||
const gridIndex = tokens.findIndex(isGrid);
|
||||
const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
|
||||
const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
|
||||
const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
|
||||
const target = directed ? callsigns[0] ?? null : null;
|
||||
return grids.map((grid) => ({ grid, station: source, source, target }));
|
||||
}
|
||||
function extractFtxCallsign(message) {
|
||||
return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
|
||||
}
|
||||
function renderFtxMessage(message) {
|
||||
let html = "";
|
||||
let index = 0;
|
||||
while (index < message.length) {
|
||||
if (!isAlphaNumeric(message[index])) {
|
||||
html += escapeFtxHtml(message[index] ?? "");
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let end = index + 1;
|
||||
while (end < message.length && isAlphaNumeric(message[end])) end += 1;
|
||||
const token = message.slice(index, end);
|
||||
const grid = token.toUpperCase();
|
||||
html += isGrid(grid) ? `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>` : escapeFtxHtml(token);
|
||||
index = end;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
function initializeFtxDecoder(config) {
|
||||
const { id, label, periodMs, periodDigits = 1 } = config;
|
||||
const status = document.getElementById(`${id}-status`);
|
||||
const period = document.getElementById(`${id}-period`);
|
||||
const messagesElement = document.getElementById(`${id}-messages`);
|
||||
const filterInput = document.getElementById(`${id}-filter`);
|
||||
let filterText = "";
|
||||
let history = [];
|
||||
const renderMessage = (message) => {
|
||||
return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
|
||||
};
|
||||
const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
|
||||
const prune = () => {
|
||||
const cutoff = Date.now() - retentionMs();
|
||||
history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
|
||||
};
|
||||
const schedule = (job) => {
|
||||
if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
|
||||
else job();
|
||||
};
|
||||
const displayFrequency = (value) => {
|
||||
const raw = finiteNumber(value);
|
||||
if (raw === null) return null;
|
||||
const base = finiteNumber(bridge.ft8BaseHz);
|
||||
return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
|
||||
};
|
||||
const renderRow = (message) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
const raw = message.message ?? "";
|
||||
row.dataset.message = raw.toUpperCase();
|
||||
row.dataset.decoder = id;
|
||||
const storedFrequency = finiteNumber(message.freq_hz);
|
||||
row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
row.innerHTML = `<span class="ft8-time">${time}</span><span class="ft8-snr">${snr?.toFixed(1) ?? "--"}</span><span class="ft8-dt">${delta?.toFixed(2) ?? "--"}</span><span class="ft8-freq">${frequency?.toFixed(0) ?? "--"}</span><span class="ft8-msg">${renderMessage(raw)}</span>`;
|
||||
return row;
|
||||
};
|
||||
const render = () => {
|
||||
prune();
|
||||
if (!messagesElement) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let count = 0;
|
||||
for (const message of history) {
|
||||
if (count >= 200) break;
|
||||
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
||||
fragment.appendChild(renderRow(message));
|
||||
count += 1;
|
||||
}
|
||||
messagesElement.replaceChildren(fragment);
|
||||
};
|
||||
const normalize = (message) => {
|
||||
const raw = message.message ?? "";
|
||||
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
||||
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
||||
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
if (grids.length > 0) {
|
||||
bridge.mapAddLocator?.(raw, grids, id, station, {
|
||||
...message,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
locator_details: locatorDetails
|
||||
});
|
||||
}
|
||||
return {
|
||||
receiver: bridge.getDecodeRigMeta?.() ?? null,
|
||||
ts_ms: message.ts_ms,
|
||||
snr_db: message.snr_db,
|
||||
dt_s: message.dt_s,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
message: message.message,
|
||||
_tsMs: finiteNumber(message.ts_ms) ?? Date.now()
|
||||
};
|
||||
};
|
||||
const receiveBatch = (messages) => {
|
||||
if (messages.length === 0) return;
|
||||
if (status) status.textContent = "Receiving";
|
||||
history = messages.map(normalize).reverse().concat(history);
|
||||
prune();
|
||||
bridge.setFt8FamilyBarDecoder?.(id);
|
||||
bridge.updateFt8Bar?.();
|
||||
schedule(render);
|
||||
};
|
||||
const reset = () => {
|
||||
history = [];
|
||||
bridge.updateFt8Bar?.();
|
||||
render();
|
||||
bridge.clearMapMarkersByType?.(id);
|
||||
};
|
||||
const barFrames = () => {
|
||||
const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
|
||||
let html = "";
|
||||
for (const message of recent) {
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "" : `<span class="aprs-bar-time">${bridge.fmtTime?.(timestamp) ?? ""}</span>`;
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
|
||||
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${time}<span class="aprs-bar-call">${renderMessage(message.message ?? "")}</span>${detail ? ` · ${detail}` : ""}</div></div>`;
|
||||
}
|
||||
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
|
||||
};
|
||||
bridge.trxPluginRuntime.registerDecoder({
|
||||
id,
|
||||
onMessage: (message) => {
|
||||
receiveBatch([message]);
|
||||
},
|
||||
onBatch: receiveBatch,
|
||||
restore: receiveBatch,
|
||||
prune: () => {
|
||||
prune();
|
||||
render();
|
||||
},
|
||||
reset
|
||||
});
|
||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||
const updatePeriod = () => {
|
||||
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
|
||||
};
|
||||
updatePeriod();
|
||||
window.setInterval(updatePeriod, 250);
|
||||
filterInput?.addEventListener("input", () => {
|
||||
filterText = filterInput.value.trim().toUpperCase();
|
||||
render();
|
||||
});
|
||||
messagesElement?.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof Element)) return;
|
||||
const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
|
||||
if (grid) {
|
||||
bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const toggle = document.getElementById(`${id}-decode-toggle-btn`);
|
||||
toggle?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
|
||||
await bridge.postPath?.(`/toggle_${id}_decode`);
|
||||
} catch (error) {
|
||||
console.error(`${label} toggle failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
await bridge.postPath?.(`/clear_${id}_decode`);
|
||||
reset();
|
||||
} catch (error) {
|
||||
console.error(`${label} history clear failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
import {
|
||||
initializeFtxDecoder
|
||||
} from "./chunk-SGMG5LG2.js";
|
||||
|
||||
// src/plugins/ft4.ts
|
||||
initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
|
||||
})();
|
||||
// src/plugins/ft4.ts
|
||||
initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
|
||||
|
||||
@@ -1,271 +1,10 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/ftx-family.ts
|
||||
var bridge = window;
|
||||
function finiteNumber(value) {
|
||||
const number = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
function isAlphaNumeric(value) {
|
||||
return value !== void 0 && /[A-Za-z0-9]/.test(value);
|
||||
}
|
||||
function isGrid(value) {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
|
||||
}
|
||||
function escapeFtxHtml(input) {
|
||||
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
function extractFtxGrids(message) {
|
||||
return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
|
||||
}
|
||||
function tokenize(message) {
|
||||
return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
|
||||
}
|
||||
function isCallsign(token) {
|
||||
return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
|
||||
}
|
||||
function extractFtxLocatorDetails(message) {
|
||||
const tokens = tokenize(message);
|
||||
const grids = extractFtxGrids(message);
|
||||
const gridIndex = tokens.findIndex(isGrid);
|
||||
const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
|
||||
const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
|
||||
const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
|
||||
const target = directed ? callsigns[0] ?? null : null;
|
||||
return grids.map((grid) => ({ grid, station: source, source, target }));
|
||||
}
|
||||
function extractFtxCallsign(message) {
|
||||
return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
|
||||
}
|
||||
function renderFtxMessage(message) {
|
||||
let html = "";
|
||||
let index = 0;
|
||||
while (index < message.length) {
|
||||
if (!isAlphaNumeric(message[index])) {
|
||||
html += escapeFtxHtml(message[index] ?? "");
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let end = index + 1;
|
||||
while (end < message.length && isAlphaNumeric(message[end])) end += 1;
|
||||
const token = message.slice(index, end);
|
||||
const grid = token.toUpperCase();
|
||||
html += isGrid(grid) ? `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>` : escapeFtxHtml(token);
|
||||
index = end;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
function installFtxCompatibilityHelpers() {
|
||||
bridge.renderFt8Message = renderFtxMessage;
|
||||
bridge.ft8EscapeHtml = escapeFtxHtml;
|
||||
bridge.ft8ExtractLocatorDetails = extractFtxLocatorDetails;
|
||||
bridge.ft8ExtractAllGrids = extractFtxGrids;
|
||||
bridge.ft8ExtractLikelyCallsign = extractFtxCallsign;
|
||||
}
|
||||
function initializeFt8FamilyBar() {
|
||||
const labels = { ft8: "FT8", ft4: "FT4", ft2: "FT2" };
|
||||
const builders = {};
|
||||
const dismissed = { ft8: 0, ft4: 0, ft2: 0 };
|
||||
const overlay = document.getElementById("ft8-bar-overlay");
|
||||
let active = "ft8";
|
||||
const update = () => {
|
||||
if (!overlay) return;
|
||||
const mode = (document.getElementById("mode")?.value ?? "").toUpperCase();
|
||||
const result = builders[active]?.();
|
||||
if (mode !== "DIG" && mode !== "USB" || !result || result.count === 0 || result.newestTsMs <= dismissed[active]) {
|
||||
overlay.style.display = "none";
|
||||
overlay.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const label = labels[active];
|
||||
overlay.innerHTML = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">${label}</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-actions"><span class="aprs-bar-window">Last 15 minutes</span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearFt8Bar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearFt8Bar();}" aria-label="Clear ${label} overlay">Clear</span></span><button class="aprs-bar-close" type="button" onclick="window.closeFt8Bar()" aria-label="Close ${label} overlay">×</button></span></div>${result.html}`;
|
||||
overlay.style.display = "flex";
|
||||
};
|
||||
bridge.registerFt8FamilyBarRenderer = (decoder, builder) => {
|
||||
builders[decoder] = builder;
|
||||
};
|
||||
bridge.setFt8FamilyBarDecoder = (decoder) => {
|
||||
active = decoder;
|
||||
update();
|
||||
};
|
||||
bridge.updateFt8Bar = update;
|
||||
bridge.clearFt8Bar = () => {
|
||||
bridge.trxPluginRuntime.reset(active);
|
||||
};
|
||||
bridge.closeFt8Bar = () => {
|
||||
dismissed[active] = Date.now();
|
||||
update();
|
||||
};
|
||||
}
|
||||
function initializeFtxDecoder(config) {
|
||||
const { id, label, periodMs, periodDigits = 1 } = config;
|
||||
const status = document.getElementById(`${id}-status`);
|
||||
const period = document.getElementById(`${id}-period`);
|
||||
const messagesElement = document.getElementById(`${id}-messages`);
|
||||
const filterInput = document.getElementById(`${id}-filter`);
|
||||
let filterText = "";
|
||||
let history = [];
|
||||
const renderMessage = (message) => {
|
||||
return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
|
||||
};
|
||||
const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
|
||||
const prune = () => {
|
||||
const cutoff = Date.now() - retentionMs();
|
||||
history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
|
||||
};
|
||||
const schedule = (job) => {
|
||||
if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
|
||||
else job();
|
||||
};
|
||||
const displayFrequency = (value) => {
|
||||
const raw = finiteNumber(value);
|
||||
if (raw === null) return null;
|
||||
const base = finiteNumber(bridge.ft8BaseHz);
|
||||
return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
|
||||
};
|
||||
const renderRow = (message) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
const raw = message.message ?? "";
|
||||
row.dataset.message = raw.toUpperCase();
|
||||
row.dataset.decoder = id;
|
||||
const storedFrequency = finiteNumber(message.freq_hz);
|
||||
row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
row.innerHTML = `<span class="ft8-time">${time}</span><span class="ft8-snr">${snr?.toFixed(1) ?? "--"}</span><span class="ft8-dt">${delta?.toFixed(2) ?? "--"}</span><span class="ft8-freq">${frequency?.toFixed(0) ?? "--"}</span><span class="ft8-msg">${renderMessage(raw)}</span>`;
|
||||
return row;
|
||||
};
|
||||
const render = () => {
|
||||
prune();
|
||||
if (!messagesElement) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let count = 0;
|
||||
for (const message of history) {
|
||||
if (count >= 200) break;
|
||||
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
||||
fragment.appendChild(renderRow(message));
|
||||
count += 1;
|
||||
}
|
||||
messagesElement.replaceChildren(fragment);
|
||||
};
|
||||
const normalize = (message) => {
|
||||
const raw = message.message ?? "";
|
||||
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
||||
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
||||
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
if (grids.length > 0) {
|
||||
bridge.mapAddLocator?.(raw, grids, id, station, {
|
||||
...message,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
locator_details: locatorDetails
|
||||
});
|
||||
}
|
||||
return {
|
||||
receiver: bridge.getDecodeRigMeta?.() ?? null,
|
||||
ts_ms: message.ts_ms,
|
||||
snr_db: message.snr_db,
|
||||
dt_s: message.dt_s,
|
||||
freq_hz: frequency ?? message.freq_hz,
|
||||
message: message.message,
|
||||
_tsMs: finiteNumber(message.ts_ms) ?? Date.now()
|
||||
};
|
||||
};
|
||||
const receiveBatch = (messages) => {
|
||||
if (messages.length === 0) return;
|
||||
if (status) status.textContent = "Receiving";
|
||||
history = messages.map(normalize).reverse().concat(history);
|
||||
prune();
|
||||
bridge.setFt8FamilyBarDecoder?.(id);
|
||||
bridge.updateFt8Bar?.();
|
||||
schedule(render);
|
||||
};
|
||||
const reset = () => {
|
||||
history = [];
|
||||
bridge.updateFt8Bar?.();
|
||||
render();
|
||||
bridge.clearMapMarkersByType?.(id);
|
||||
};
|
||||
const barFrames = () => {
|
||||
const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
|
||||
let html = "";
|
||||
for (const message of recent) {
|
||||
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
|
||||
const time = timestamp === null ? "" : `<span class="aprs-bar-time">${bridge.fmtTime?.(timestamp) ?? ""}</span>`;
|
||||
const snr = finiteNumber(message.snr_db);
|
||||
const delta = finiteNumber(message.dt_s);
|
||||
const frequency = displayFrequency(message.freq_hz);
|
||||
const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
|
||||
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${time}<span class="aprs-bar-call">${renderMessage(message.message ?? "")}</span>${detail ? ` · ${detail}` : ""}</div></div>`;
|
||||
}
|
||||
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
|
||||
};
|
||||
bridge.trxPluginRuntime.registerDecoder({
|
||||
id,
|
||||
onMessage: (message) => {
|
||||
receiveBatch([message]);
|
||||
},
|
||||
onBatch: receiveBatch,
|
||||
restore: receiveBatch,
|
||||
prune: () => {
|
||||
prune();
|
||||
render();
|
||||
},
|
||||
reset
|
||||
});
|
||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||
const updatePeriod = () => {
|
||||
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
|
||||
};
|
||||
updatePeriod();
|
||||
window.setInterval(updatePeriod, 250);
|
||||
filterInput?.addEventListener("input", () => {
|
||||
filterText = filterInput.value.trim().toUpperCase();
|
||||
render();
|
||||
});
|
||||
messagesElement?.addEventListener("click", (event) => {
|
||||
if (!(event.target instanceof Element)) return;
|
||||
const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
|
||||
if (grid) {
|
||||
bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const toggle = document.getElementById(`${id}-decode-toggle-btn`);
|
||||
toggle?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
|
||||
await bridge.postPath?.(`/toggle_${id}_decode`);
|
||||
} catch (error) {
|
||||
console.error(`${label} toggle failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
await bridge.postPath?.(`/clear_${id}_decode`);
|
||||
reset();
|
||||
} catch (error) {
|
||||
console.error(`${label} history clear failed`, error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
import {
|
||||
initializeFt8FamilyBar,
|
||||
initializeFtxDecoder,
|
||||
installFtxCompatibilityHelpers
|
||||
} from "./chunk-SGMG5LG2.js";
|
||||
|
||||
// src/plugins/ft8.ts
|
||||
installFtxCompatibilityHelpers();
|
||||
initializeFt8FamilyBar();
|
||||
initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 });
|
||||
})();
|
||||
// src/plugins/ft8.ts
|
||||
installFtxCompatibilityHelpers();
|
||||
initializeFt8FamilyBar();
|
||||
initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 });
|
||||
|
||||
@@ -1,133 +1,40 @@
|
||||
"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)) : `<span class="aprs-byte">0x${byte.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
||||
}
|
||||
function renderAprsCharacter(character) {
|
||||
const code = character.charCodeAt(0);
|
||||
return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `<span class="aprs-byte">0x${code.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
||||
}
|
||||
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 `<span class="aprs-symbol aprs-symbol-local" title="${table} APRS symbol ${symbol}">${symbol}</span>`;
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
import {
|
||||
aprsAgeText,
|
||||
aprsCategoryLabel,
|
||||
aprsHexBytes,
|
||||
aprsPacketCategory,
|
||||
collapseAprsDuplicates,
|
||||
normalizeAprsPacket,
|
||||
renderAprsInfo,
|
||||
renderLocalAprsSymbol
|
||||
} from "./chunk-M2I6DH4X.js";
|
||||
|
||||
// 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() {
|
||||
// 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() {
|
||||
}
|
||||
function pruneHfAprsPacketHistory() {
|
||||
const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs();
|
||||
hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
|
||||
}
|
||||
function scheduleHfAprsHistoryRender() {
|
||||
}
|
||||
function scheduleHfAprsHistoryRender() {
|
||||
if (typeof hfAprsWindow.trxScheduleUiFrameJob === "function") {
|
||||
hfAprsWindow.trxScheduleUiFrameJob("hf-aprs-history", () => {
|
||||
renderHfAprsHistory();
|
||||
@@ -135,15 +42,15 @@
|
||||
return;
|
||||
}
|
||||
renderHfAprsHistory();
|
||||
}
|
||||
function hfAprsDistanceText(pkt) {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
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;
|
||||
@@ -159,13 +66,13 @@
|
||||
aprsPacketCategory(pkt)
|
||||
].filter(Boolean).join(" ").toUpperCase();
|
||||
return haystack.includes(hfAprsFilterText);
|
||||
}
|
||||
function hfAprsVisiblePackets() {
|
||||
}
|
||||
function hfAprsVisiblePackets() {
|
||||
const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
|
||||
return packets.filter(hfAprsFilterMatch);
|
||||
}
|
||||
var collapseHfAprsDuplicates = collapseAprsDuplicates;
|
||||
function updateHfAprsSummary() {
|
||||
}
|
||||
var collapseHfAprsDuplicates = collapseAprsDuplicates;
|
||||
function updateHfAprsSummary() {
|
||||
const visible = hfAprsVisiblePackets();
|
||||
if (hfAprsTotalCountEl) {
|
||||
hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
|
||||
@@ -181,16 +88,16 @@
|
||||
hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateHfAprsChipState() {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
function renderHfAprsRow(pkt, isFresh) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "aprs-packet";
|
||||
if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
|
||||
@@ -236,8 +143,8 @@
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}
|
||||
function renderHfAprsHistory() {
|
||||
}
|
||||
function renderHfAprsHistory() {
|
||||
pruneHfAprsPacketHistory();
|
||||
if (!hfAprsPacketsEl) {
|
||||
updateHfAprsSummary();
|
||||
@@ -252,28 +159,28 @@
|
||||
hfAprsPacketsEl.replaceChildren(fragment);
|
||||
updateHfAprsSummary();
|
||||
updateHfAprsChipState();
|
||||
}
|
||||
function resetHfAprsHistoryView() {
|
||||
}
|
||||
function resetHfAprsHistoryView() {
|
||||
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
||||
hfAprsPacketHistory = [];
|
||||
renderHfAprsHistory();
|
||||
}
|
||||
function pruneHfAprsHistoryView() {
|
||||
}
|
||||
function pruneHfAprsHistoryView() {
|
||||
pruneHfAprsPacketHistory();
|
||||
renderHfAprsHistory();
|
||||
}
|
||||
function addHfAprsPacket(pkt) {
|
||||
}
|
||||
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) {
|
||||
}
|
||||
function normalizeServerHfAprsPacket(pkt) {
|
||||
return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
|
||||
}
|
||||
function onServerHfAprsBatch(packets) {
|
||||
}
|
||||
function onServerHfAprsBatch(packets) {
|
||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||
const normalized = [];
|
||||
@@ -288,9 +195,9 @@
|
||||
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
|
||||
pruneHfAprsPacketHistory();
|
||||
scheduleHfAprsHistoryRender();
|
||||
}
|
||||
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
||||
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
|
||||
}
|
||||
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
||||
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn);
|
||||
@@ -299,8 +206,8 @@
|
||||
console.error("HF APRS toggle failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", () => {
|
||||
});
|
||||
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 {
|
||||
@@ -310,50 +217,49 @@
|
||||
console.error("HF APRS history clear failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
if (hfAprsOnlyPosBtn) {
|
||||
});
|
||||
if (hfAprsOnlyPosBtn) {
|
||||
hfAprsOnlyPosBtn.addEventListener("click", () => {
|
||||
hfAprsOnlyPos = !hfAprsOnlyPos;
|
||||
renderHfAprsHistory();
|
||||
});
|
||||
}
|
||||
if (hfAprsHideCrcBtn) {
|
||||
}
|
||||
if (hfAprsHideCrcBtn) {
|
||||
hfAprsHideCrcBtn.addEventListener("click", () => {
|
||||
hfAprsHideCrc = !hfAprsHideCrc;
|
||||
renderHfAprsHistory();
|
||||
});
|
||||
}
|
||||
if (hfAprsCollapseDupBtn) {
|
||||
}
|
||||
if (hfAprsCollapseDupBtn) {
|
||||
hfAprsCollapseDupBtn.addEventListener("click", () => {
|
||||
hfAprsCollapseDup = !hfAprsCollapseDup;
|
||||
renderHfAprsHistory();
|
||||
});
|
||||
}
|
||||
["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
|
||||
}
|
||||
["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) {
|
||||
});
|
||||
if (hfAprsFilterInput) {
|
||||
hfAprsFilterInput.addEventListener("input", () => {
|
||||
hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase();
|
||||
renderHfAprsHistory();
|
||||
});
|
||||
}
|
||||
function onServerHfAprs(pkt) {
|
||||
}
|
||||
function onServerHfAprs(pkt) {
|
||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
||||
}
|
||||
renderHfAprsHistory();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
renderHfAprsHistory();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
id: "hf_aprs",
|
||||
onMessage: onServerHfAprs,
|
||||
onBatch: onServerHfAprsBatch,
|
||||
restore: onServerHfAprsBatch,
|
||||
reset: resetHfAprsHistoryView,
|
||||
prune: pruneHfAprsHistoryView
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
+2
-5
@@ -1,7 +1,5 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/leaflet-ais-tracksymbol.ts
|
||||
(function() {
|
||||
// src/leaflet-ais-tracksymbol.ts
|
||||
(function() {
|
||||
const leaflet = globalThis.L;
|
||||
if (!leaflet) return;
|
||||
function clamp(value, min, max) {
|
||||
@@ -94,5 +92,4 @@
|
||||
if (!Constructor) throw new Error("AIS track symbol constructor is unavailable");
|
||||
return new Constructor(latlng, options);
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/map-core.ts
|
||||
function mapEl(id) {
|
||||
// src/map-core.ts
|
||||
function mapEl(id) {
|
||||
const element = document.querySelector(`#${CSS.escape(id)}`);
|
||||
if (!element) throw new Error(`Missing map element #${id}`);
|
||||
return element;
|
||||
}
|
||||
var mapWindow = window;
|
||||
(function() {
|
||||
}
|
||||
var mapWindow = window;
|
||||
(function() {
|
||||
"use strict";
|
||||
const { state: T, core: C, modules } = mapWindow.trx;
|
||||
const {
|
||||
@@ -3110,5 +3108,4 @@
|
||||
reverseGeocodeLocation
|
||||
};
|
||||
autoInitIfVisible();
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/sat-scheduler.ts
|
||||
var satSchedulerWindow = window;
|
||||
(function() {
|
||||
// src/plugins/sat-scheduler.ts
|
||||
var satSchedulerWindow = window;
|
||||
(function() {
|
||||
"use strict";
|
||||
const dom = {
|
||||
enabled: document.getElementById("scheduler-sat-enabled"),
|
||||
@@ -262,5 +260,4 @@
|
||||
wireEvents();
|
||||
renderSection();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/sat.ts
|
||||
var satWindow = window;
|
||||
var satDom = {
|
||||
// src/plugins/sat.ts
|
||||
var satWindow = window;
|
||||
var satDom = {
|
||||
status: document.getElementById("sat-status"),
|
||||
liveView: document.getElementById("sat-live-view"),
|
||||
historyView: document.getElementById("sat-history-view"),
|
||||
@@ -25,27 +23,27 @@
|
||||
predCurrentSec: document.getElementById("sat-pred-current-section"),
|
||||
predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
|
||||
predStatus: document.getElementById("sat-pred-status")
|
||||
};
|
||||
var satImageHistory = [];
|
||||
var SAT_MAX_IMAGES = 100;
|
||||
var SAT_PRED_PAGE_SIZE = 50;
|
||||
var satPredShowAll = false;
|
||||
var satFilterText = "";
|
||||
var satActiveView = "live";
|
||||
var satPredData = [];
|
||||
var satPredFilterText = "";
|
||||
var satPredMinEl = 0;
|
||||
var satPredCategory = "all";
|
||||
var satPredSatCount = 0;
|
||||
var satPredCountdownTimer = null;
|
||||
function scheduleSatUi(key, job) {
|
||||
};
|
||||
var satImageHistory = [];
|
||||
var SAT_MAX_IMAGES = 100;
|
||||
var SAT_PRED_PAGE_SIZE = 50;
|
||||
var satPredShowAll = false;
|
||||
var satFilterText = "";
|
||||
var satActiveView = "live";
|
||||
var satPredData = [];
|
||||
var satPredFilterText = "";
|
||||
var satPredMinEl = 0;
|
||||
var satPredCategory = "all";
|
||||
var satPredSatCount = 0;
|
||||
var satPredCountdownTimer = null;
|
||||
function scheduleSatUi(key, job) {
|
||||
if (typeof satWindow.trxScheduleUiFrameJob === "function") {
|
||||
satWindow.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
function switchSatView(view) {
|
||||
}
|
||||
function switchSatView(view) {
|
||||
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
|
||||
satActiveView = view;
|
||||
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
|
||||
@@ -61,24 +59,24 @@
|
||||
satPredShowAll = false;
|
||||
void loadSatPredictions();
|
||||
}
|
||||
}
|
||||
function clearPredictionDom() {
|
||||
}
|
||||
function clearPredictionDom() {
|
||||
stopCountdownTimer();
|
||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||
}
|
||||
satWindow.clearSatPredictionDom = clearPredictionDom;
|
||||
satDom.viewLiveBtn?.addEventListener("click", () => {
|
||||
}
|
||||
satWindow.clearSatPredictionDom = clearPredictionDom;
|
||||
satDom.viewLiveBtn?.addEventListener("click", () => {
|
||||
switchSatView("live");
|
||||
});
|
||||
satDom.viewHistoryBtn?.addEventListener("click", () => {
|
||||
});
|
||||
satDom.viewHistoryBtn?.addEventListener("click", () => {
|
||||
switchSatView("history");
|
||||
});
|
||||
satDom.viewPredBtn?.addEventListener("click", () => {
|
||||
});
|
||||
satDom.viewPredBtn?.addEventListener("click", () => {
|
||||
switchSatView("predictions");
|
||||
});
|
||||
var lastSatLrptOn = null;
|
||||
satWindow.updateSatLiveState = function(update) {
|
||||
});
|
||||
var lastSatLrptOn = null;
|
||||
satWindow.updateSatLiveState = function(update) {
|
||||
if (!satDom.lrptState) return;
|
||||
const lrptOn = !!update.lrpt_decode_enabled;
|
||||
if (lrptOn !== lastSatLrptOn) {
|
||||
@@ -93,8 +91,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function renderSatLatestCard() {
|
||||
};
|
||||
function renderSatLatestCard() {
|
||||
if (!satDom.liveLatest) return;
|
||||
if (satImageHistory.length === 0) {
|
||||
satDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable a decoder and wait for a satellite pass.</div>';
|
||||
@@ -125,8 +123,8 @@
|
||||
}
|
||||
html += `</div>`;
|
||||
satDom.liveLatest.innerHTML = html;
|
||||
}
|
||||
function getSatFilteredHistory() {
|
||||
}
|
||||
function getSatFilteredHistory() {
|
||||
let items = satImageHistory;
|
||||
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
|
||||
if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
|
||||
@@ -145,8 +143,8 @@
|
||||
const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
|
||||
if (sortVal === "oldest") items = items.slice().reverse();
|
||||
return items;
|
||||
}
|
||||
function renderSatHistoryRow(img) {
|
||||
}
|
||||
function renderSatHistoryRow(img) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-history-row";
|
||||
const typeName = "Meteor LRPT";
|
||||
@@ -170,8 +168,8 @@
|
||||
`<span>${link}</span>`
|
||||
].join("");
|
||||
return row;
|
||||
}
|
||||
function renderSatHistoryTable() {
|
||||
}
|
||||
function renderSatHistoryTable() {
|
||||
if (!satDom.historyList) return;
|
||||
const items = getSatFilteredHistory();
|
||||
const fragment = document.createDocumentFragment();
|
||||
@@ -184,8 +182,8 @@
|
||||
const shown = items.length;
|
||||
satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
|
||||
}
|
||||
}
|
||||
function addSatImage(img, decoder) {
|
||||
}
|
||||
function addSatImage(img, decoder) {
|
||||
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
|
||||
img._tsMs = tsMs;
|
||||
img._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||
@@ -206,42 +204,42 @@
|
||||
renderSatHistoryTable();
|
||||
});
|
||||
}
|
||||
}
|
||||
function onServerLrptProgress(msg) {
|
||||
}
|
||||
function onServerLrptProgress(msg) {
|
||||
if (satDom.status && (msg.mcu_count ?? 0) > 0) {
|
||||
satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
|
||||
}
|
||||
}
|
||||
function onServerLrptImage(msg) {
|
||||
}
|
||||
function onServerLrptImage(msg) {
|
||||
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
|
||||
addSatImage(msg, "lrpt");
|
||||
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
|
||||
satWindow.addSatMapOverlay(msg);
|
||||
}
|
||||
}
|
||||
function resetSatHistoryView() {
|
||||
}
|
||||
function resetSatHistoryView() {
|
||||
satImageHistory = [];
|
||||
if (satDom.historyList) satDom.historyList.innerHTML = "";
|
||||
renderSatLatestCard();
|
||||
renderSatHistoryTable();
|
||||
satWindow.clearSatMapOverlays?.();
|
||||
}
|
||||
function pruneSatHistoryView() {
|
||||
}
|
||||
function pruneSatHistoryView() {
|
||||
renderSatHistoryTable();
|
||||
renderSatLatestCard();
|
||||
}
|
||||
satWindow.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
satWindow.trxPluginRuntime.registerDecoder({
|
||||
id: "lrpt_image",
|
||||
onMessage: onServerLrptImage,
|
||||
reset: resetSatHistoryView,
|
||||
prune: pruneSatHistoryView
|
||||
});
|
||||
satWindow.trxPluginRuntime.registerDecoder({
|
||||
});
|
||||
satWindow.trxPluginRuntime.registerDecoder({
|
||||
id: "lrpt_progress",
|
||||
onMessage: onServerLrptProgress
|
||||
});
|
||||
var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
||||
lrptDecodeToggleBtn?.addEventListener("click", () => {
|
||||
});
|
||||
var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
||||
lrptDecodeToggleBtn?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
|
||||
@@ -250,19 +248,19 @@
|
||||
console.error("LRPT toggle failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
var satFilterInput = satDom.filterInput;
|
||||
satFilterInput?.addEventListener("input", () => {
|
||||
});
|
||||
var satFilterInput = satDom.filterInput;
|
||||
satFilterInput?.addEventListener("input", () => {
|
||||
satFilterText = satFilterInput.value.trim().toUpperCase();
|
||||
renderSatHistoryTable();
|
||||
});
|
||||
satDom.sortSelect?.addEventListener("change", () => {
|
||||
});
|
||||
satDom.sortSelect?.addEventListener("change", () => {
|
||||
renderSatHistoryTable();
|
||||
});
|
||||
satDom.typeFilter?.addEventListener("change", () => {
|
||||
});
|
||||
satDom.typeFilter?.addEventListener("change", () => {
|
||||
renderSatHistoryTable();
|
||||
});
|
||||
document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
|
||||
});
|
||||
document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
@@ -272,12 +270,12 @@
|
||||
console.error("Weather satellite history clear failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
function azToCardinal(deg) {
|
||||
});
|
||||
function azToCardinal(deg) {
|
||||
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
||||
return dirs[Math.round(deg / 45) % 8] ?? "N";
|
||||
}
|
||||
function formatPredTime(ms) {
|
||||
}
|
||||
function formatPredTime(ms) {
|
||||
const d = new Date(ms);
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
@@ -285,29 +283,29 @@
|
||||
const hh = String(d.getUTCHours()).padStart(2, "0");
|
||||
const mm = String(d.getUTCMinutes()).padStart(2, "0");
|
||||
return `${day}${hh}:${mm}`;
|
||||
}
|
||||
function formatPredDuration(s) {
|
||||
}
|
||||
function formatPredDuration(s) {
|
||||
if (s >= 60) return `${Math.round(s / 60)} min`;
|
||||
return `${s}s`;
|
||||
}
|
||||
function formatCountdown(ms) {
|
||||
}
|
||||
function formatCountdown(ms) {
|
||||
const totalSec = Math.max(0, Math.floor(ms / 1e3));
|
||||
const m = Math.floor(totalSec / 60);
|
||||
const s = totalSec % 60;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
function elevationClass(deg) {
|
||||
}
|
||||
function elevationClass(deg) {
|
||||
if (deg >= 45) return "sat-pred-el-high";
|
||||
if (deg >= 10) return "sat-pred-el-mid";
|
||||
return "sat-pred-el-low";
|
||||
}
|
||||
function stopCountdownTimer() {
|
||||
}
|
||||
function stopCountdownTimer() {
|
||||
if (satPredCountdownTimer) {
|
||||
clearInterval(satPredCountdownTimer);
|
||||
satPredCountdownTimer = null;
|
||||
}
|
||||
}
|
||||
function startCountdownTimer(container) {
|
||||
}
|
||||
function startCountdownTimer(container) {
|
||||
const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
|
||||
if (countdownEls.length === 0) return;
|
||||
satPredCountdownTimer = setInterval(() => {
|
||||
@@ -332,8 +330,8 @@
|
||||
renderSatPredictions(getFilteredPredictions());
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
function buildCurrentPassRow(pass, now) {
|
||||
}
|
||||
function buildCurrentPassRow(pass, now) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-pred-row-current";
|
||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
|
||||
@@ -347,8 +345,8 @@
|
||||
`<span class="sat-pred-col-dir">${dir}</span>`
|
||||
].join("");
|
||||
return row;
|
||||
}
|
||||
function buildUpcomingPassRow(pass) {
|
||||
}
|
||||
function buildUpcomingPassRow(pass) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-pred-row";
|
||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
|
||||
@@ -360,33 +358,33 @@
|
||||
`<span class="sat-pred-col-dir">${dir}</span>`
|
||||
].join("");
|
||||
return row;
|
||||
}
|
||||
function getFilteredPredictions() {
|
||||
}
|
||||
function getFilteredPredictions() {
|
||||
let items = satPredData;
|
||||
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
|
||||
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
|
||||
if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
|
||||
return items;
|
||||
}
|
||||
function applyPredFilters() {
|
||||
}
|
||||
function applyPredFilters() {
|
||||
renderSatPredictions(getFilteredPredictions());
|
||||
}
|
||||
var satPredictionFilter = satDom.predFilter;
|
||||
satPredictionFilter?.addEventListener("input", () => {
|
||||
}
|
||||
var satPredictionFilter = satDom.predFilter;
|
||||
satPredictionFilter?.addEventListener("input", () => {
|
||||
satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
|
||||
applyPredFilters();
|
||||
});
|
||||
var satPredictionMinElevation = satDom.predMinEl;
|
||||
satPredictionMinElevation?.addEventListener("change", () => {
|
||||
});
|
||||
var satPredictionMinElevation = satDom.predMinEl;
|
||||
satPredictionMinElevation?.addEventListener("change", () => {
|
||||
satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
|
||||
applyPredFilters();
|
||||
});
|
||||
var satPredictionCategory = satDom.predCategory;
|
||||
satPredictionCategory?.addEventListener("change", () => {
|
||||
});
|
||||
var satPredictionCategory = satDom.predCategory;
|
||||
satPredictionCategory?.addEventListener("change", () => {
|
||||
satPredCategory = satPredictionCategory.value;
|
||||
applyPredFilters();
|
||||
});
|
||||
function renderSatPredictions(passes, error) {
|
||||
});
|
||||
function renderSatPredictions(passes, error) {
|
||||
stopCountdownTimer();
|
||||
if (error) {
|
||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||
@@ -446,8 +444,8 @@
|
||||
if (current.length > 0 && satActiveView === "predictions") {
|
||||
startCountdownTimer(satDom.predCurrentList);
|
||||
}
|
||||
}
|
||||
async function loadSatPredictions() {
|
||||
}
|
||||
async function loadSatPredictions() {
|
||||
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
|
||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||
@@ -466,8 +464,8 @@
|
||||
} catch (error) {
|
||||
renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
satWindow.satShowOnMap = function(south, west, north, east) {
|
||||
}
|
||||
satWindow.satShowOnMap = function(south, west, north, east) {
|
||||
if (typeof satWindow.enableMapSourceFilter === "function") {
|
||||
satWindow.enableMapSourceFilter("sat");
|
||||
}
|
||||
@@ -476,7 +474,6 @@
|
||||
if (satWindow.navigateToAprsMap) {
|
||||
satWindow.navigateToAprsMap(lat, lon);
|
||||
}
|
||||
};
|
||||
renderSatLatestCard();
|
||||
renderSatHistoryTable();
|
||||
})();
|
||||
};
|
||||
renderSatLatestCard();
|
||||
renderSatHistoryTable();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/scheduler.ts
|
||||
var schedulerWindow = window;
|
||||
var wiredElements = /* @__PURE__ */ new WeakSet();
|
||||
function schedulerEl(id) {
|
||||
// src/plugins/scheduler.ts
|
||||
var schedulerWindow = window;
|
||||
var wiredElements = /* @__PURE__ */ new WeakSet();
|
||||
function schedulerEl(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) throw new Error(`Missing scheduler element #${id}`);
|
||||
return element;
|
||||
}
|
||||
(function() {
|
||||
}
|
||||
(function() {
|
||||
"use strict";
|
||||
let schedulerRole = null;
|
||||
let currentRigId = null;
|
||||
@@ -1219,5 +1217,4 @@
|
||||
initScheduler(schedulerWindow.lastActiveRigId ?? null, schedulerWindow.authRole);
|
||||
wireSchedulerEvents();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/vchan.ts
|
||||
var vchanWindow = window;
|
||||
var vchanSessionId = null;
|
||||
var vchanRigId = null;
|
||||
var vchanChannels = [];
|
||||
var vchanActiveId = null;
|
||||
var schedulerReleaseState = null;
|
||||
var schedulerReleasePollTimer = null;
|
||||
function vchanFmtFreq(hz) {
|
||||
// src/plugins/vchan.ts
|
||||
var vchanWindow = window;
|
||||
var vchanSessionId = null;
|
||||
var vchanRigId = null;
|
||||
var vchanChannels = [];
|
||||
var vchanActiveId = null;
|
||||
var schedulerReleaseState = null;
|
||||
var schedulerReleasePollTimer = null;
|
||||
function vchanFmtFreq(hz) {
|
||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||
if (hz >= 1e9) return (hz / 1e9).toFixed(4).replace(/\.?0+$/, "") + " GHz";
|
||||
if (hz >= 1e6) return (hz / 1e6).toFixed(4).replace(/\.?0+$/, "") + " MHz";
|
||||
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
|
||||
return `${String(hz)} Hz`;
|
||||
}
|
||||
function schedulerReleaseSummaryText(state) {
|
||||
}
|
||||
function schedulerReleaseSummaryText(state) {
|
||||
if (!state) return "Scheduler is controlling the rig.";
|
||||
const connected = Number(state.connected_sessions) || 0;
|
||||
const released = Number(state.released_sessions) || 0;
|
||||
@@ -29,8 +27,8 @@
|
||||
}
|
||||
const blocking = Math.max(connected - released, 0);
|
||||
return blocking > 0 ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.` : "Scheduler can control the rig.";
|
||||
}
|
||||
function vchanRenderSchedulerRelease() {
|
||||
}
|
||||
function vchanRenderSchedulerRelease() {
|
||||
const btn = document.getElementById("scheduler-release-btn");
|
||||
const status = document.getElementById("scheduler-release-status");
|
||||
if (!btn || !status) return;
|
||||
@@ -39,8 +37,8 @@
|
||||
btn.classList.toggle("active", !currentReleased);
|
||||
btn.textContent = "Release to Scheduler";
|
||||
status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
|
||||
}
|
||||
async function vchanPollSchedulerRelease() {
|
||||
}
|
||||
async function vchanPollSchedulerRelease() {
|
||||
if (!vchanSessionId) {
|
||||
schedulerReleaseState = null;
|
||||
vchanRenderSchedulerRelease();
|
||||
@@ -54,16 +52,16 @@
|
||||
} catch (e) {
|
||||
console.error("scheduler release status failed", e);
|
||||
}
|
||||
}
|
||||
function vchanStartSchedulerReleasePolling() {
|
||||
}
|
||||
function vchanStartSchedulerReleasePolling() {
|
||||
if (schedulerReleasePollTimer) {
|
||||
clearInterval(schedulerReleasePollTimer);
|
||||
}
|
||||
schedulerReleasePollTimer = setInterval(() => {
|
||||
void vchanPollSchedulerRelease();
|
||||
}, 1e4);
|
||||
}
|
||||
async function vchanToggleSchedulerRelease() {
|
||||
}
|
||||
async function vchanToggleSchedulerRelease() {
|
||||
if (!vchanSessionId) return;
|
||||
const rigId = vchanRigId || vchanWindow.lastActiveRigId || null;
|
||||
try {
|
||||
@@ -78,8 +76,8 @@
|
||||
} catch (e) {
|
||||
console.error("scheduler release toggle failed", e);
|
||||
}
|
||||
}
|
||||
async function vchanTakeSchedulerControl() {
|
||||
}
|
||||
async function vchanTakeSchedulerControl() {
|
||||
if (!vchanSessionId) return;
|
||||
if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return;
|
||||
try {
|
||||
@@ -94,9 +92,9 @@
|
||||
} catch (e) {
|
||||
console.error("scheduler control takeover failed", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanTakeSchedulerControl = vchanTakeSchedulerControl;
|
||||
function vchanHandleSession(data) {
|
||||
}
|
||||
vchanWindow.vchanTakeSchedulerControl = vchanTakeSchedulerControl;
|
||||
function vchanHandleSession(data) {
|
||||
try {
|
||||
const d = JSON.parse(data);
|
||||
vchanSessionId = d.session_id || null;
|
||||
@@ -104,8 +102,8 @@
|
||||
} catch (e) {
|
||||
console.warn("vchan: bad session event", e);
|
||||
}
|
||||
}
|
||||
function vchanHandleChannels(data) {
|
||||
}
|
||||
function vchanHandleChannels(data) {
|
||||
try {
|
||||
const d = JSON.parse(data);
|
||||
vchanRigId = d.remote || null;
|
||||
@@ -124,10 +122,10 @@
|
||||
} catch (e) {
|
||||
console.warn("vchan: bad channels event", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanHandleSession = vchanHandleSession;
|
||||
vchanWindow.vchanHandleChannels = vchanHandleChannels;
|
||||
function vchanRender() {
|
||||
}
|
||||
vchanWindow.vchanHandleSession = vchanHandleSession;
|
||||
vchanWindow.vchanHandleChannels = vchanHandleChannels;
|
||||
function vchanRender() {
|
||||
const picker = document.getElementById("vchan-picker");
|
||||
if (!picker) return;
|
||||
picker.innerHTML = "";
|
||||
@@ -170,8 +168,8 @@
|
||||
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
|
||||
}
|
||||
vchanRenderSchedulerRelease();
|
||||
}
|
||||
async function vchanAllocate() {
|
||||
}
|
||||
async function vchanAllocate() {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
const freqHz = typeof vchanWindow.lastFreqHz === "number" && vchanWindow.lastFreqHz > 0 ? vchanWindow.lastFreqHz : 0;
|
||||
const modeEl = document.getElementById("mode");
|
||||
@@ -194,8 +192,8 @@
|
||||
} catch (e) {
|
||||
console.error("vchan: allocate error", e);
|
||||
}
|
||||
}
|
||||
async function vchanDelete(channelId) {
|
||||
}
|
||||
async function vchanDelete(channelId) {
|
||||
if (!vchanRigId) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
@@ -208,8 +206,8 @@
|
||||
} catch (e) {
|
||||
console.error("vchan: delete error", e);
|
||||
}
|
||||
}
|
||||
async function vchanAutoJoinPrimary(channelId) {
|
||||
}
|
||||
async function vchanAutoJoinPrimary(channelId) {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
@@ -229,8 +227,8 @@
|
||||
} catch (e) {
|
||||
console.error("vchan: auto-join error", e);
|
||||
}
|
||||
}
|
||||
async function vchanSubscribe(channelId) {
|
||||
}
|
||||
async function vchanSubscribe(channelId) {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
@@ -253,8 +251,8 @@
|
||||
} catch (e) {
|
||||
console.error("vchan: subscribe error", e);
|
||||
}
|
||||
}
|
||||
function vchanReconnectAudio() {
|
||||
}
|
||||
function vchanReconnectAudio() {
|
||||
const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
|
||||
vchanWindow._audioChannelOverride = ch?.id ?? null;
|
||||
if (!vchanWindow.rxActive) return;
|
||||
@@ -262,23 +260,23 @@
|
||||
setTimeout(() => {
|
||||
vchanWindow.startRxAudio?.();
|
||||
}, 300);
|
||||
}
|
||||
function vchanApplyCapabilities(caps) {
|
||||
}
|
||||
function vchanApplyCapabilities(caps) {
|
||||
const picker = document.getElementById("vchan-picker");
|
||||
if (!picker) return;
|
||||
picker.style.display = caps && caps.filter_controls ? "" : "none";
|
||||
vchanRenderSchedulerRelease();
|
||||
}
|
||||
vchanWindow.vchanApplyCapabilities = vchanApplyCapabilities;
|
||||
function vchanIsOnVirtual() {
|
||||
}
|
||||
vchanWindow.vchanApplyCapabilities = vchanApplyCapabilities;
|
||||
function vchanIsOnVirtual() {
|
||||
if (!vchanActiveId || vchanChannels.length === 0) return false;
|
||||
return vchanActiveId !== vchanChannels[0]?.id;
|
||||
}
|
||||
vchanWindow.vchanIsOnVirtual = vchanIsOnVirtual;
|
||||
function vchanActiveChannel() {
|
||||
}
|
||||
vchanWindow.vchanIsOnVirtual = vchanIsOnVirtual;
|
||||
function vchanActiveChannel() {
|
||||
return vchanChannels.find((c) => c.id === vchanActiveId) || null;
|
||||
}
|
||||
function vchanUpdateFreqDisplay() {
|
||||
}
|
||||
function vchanUpdateFreqDisplay() {
|
||||
const ch = vchanActiveChannel();
|
||||
if (!ch) return;
|
||||
const el = document.getElementById("freq");
|
||||
@@ -288,8 +286,8 @@
|
||||
} else {
|
||||
el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
|
||||
}
|
||||
}
|
||||
function vchanSyncModeDisplay() {
|
||||
}
|
||||
function vchanSyncModeDisplay() {
|
||||
const modeEl = document.getElementById("mode");
|
||||
if (!modeEl) return;
|
||||
if (vchanIsOnVirtual()) {
|
||||
@@ -313,8 +311,8 @@
|
||||
} else {
|
||||
vchanWindow.positionRdsPsOverlay?.();
|
||||
}
|
||||
}
|
||||
function vchanSyncBwDisplay() {
|
||||
}
|
||||
function vchanSyncBwDisplay() {
|
||||
if (!vchanIsOnVirtual()) return;
|
||||
const ch = vchanActiveChannel();
|
||||
if (!ch) return;
|
||||
@@ -328,8 +326,8 @@
|
||||
bwEl.value = (bwHz / 1e3).toFixed(3).replace(/\.?0+$/, "");
|
||||
vchanWindow.currentBandwidthHz = bwHz;
|
||||
}
|
||||
}
|
||||
function vchanSyncAccentUI() {
|
||||
}
|
||||
function vchanSyncAccentUI() {
|
||||
const onVirtual = vchanIsOnVirtual();
|
||||
const freqEl = document.getElementById("freq");
|
||||
const bwEl = document.getElementById("spectrum-bw-input");
|
||||
@@ -345,9 +343,9 @@
|
||||
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
|
||||
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
|
||||
}
|
||||
}
|
||||
var origRefreshFreqDisplay = null;
|
||||
function vchanSetChannelFreq(freqHz) {
|
||||
}
|
||||
var origRefreshFreqDisplay = null;
|
||||
function vchanSetChannelFreq(freqHz) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
if (vchanWindow.lastSpectrumData && vchanWindow.lastSpectrumData.sample_rate > 0) {
|
||||
const halfSpan = vchanWindow.lastSpectrumData.sample_rate / 2;
|
||||
@@ -373,8 +371,8 @@
|
||||
).catch((error) => {
|
||||
console.error("vchan: set freq error", error);
|
||||
});
|
||||
}
|
||||
async function vchanSetChannelBandwidth(bwHz) {
|
||||
}
|
||||
async function vchanSetChannelBandwidth(bwHz) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
@@ -390,8 +388,8 @@
|
||||
} catch (e) {
|
||||
console.error("vchan: set bw error", e);
|
||||
}
|
||||
}
|
||||
async function vchanSetChannelMode(mode) {
|
||||
}
|
||||
async function vchanSetChannelMode(mode) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
@@ -407,18 +405,18 @@
|
||||
} catch (e) {
|
||||
console.error("vchan: set mode error", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanInterceptMode = async function(mode) {
|
||||
}
|
||||
vchanWindow.vchanInterceptMode = async function(mode) {
|
||||
if (!vchanIsOnVirtual()) return false;
|
||||
await vchanSetChannelMode(mode);
|
||||
return true;
|
||||
};
|
||||
vchanWindow.vchanInterceptBandwidth = async function(bwHz) {
|
||||
};
|
||||
vchanWindow.vchanInterceptBandwidth = async function(bwHz) {
|
||||
if (!vchanIsOnVirtual()) return false;
|
||||
await vchanSetChannelBandwidth(bwHz);
|
||||
return true;
|
||||
};
|
||||
(function() {
|
||||
};
|
||||
(function() {
|
||||
const original = vchanWindow.setRigFrequency;
|
||||
vchanWindow.setRigFrequency = function(freqHz) {
|
||||
if (vchanIsOnVirtual()) {
|
||||
@@ -435,8 +433,8 @@
|
||||
void vchanTakeSchedulerControl();
|
||||
original?.(freqHz);
|
||||
};
|
||||
})();
|
||||
(function initSchedulerReleaseControl() {
|
||||
})();
|
||||
(function initSchedulerReleaseControl() {
|
||||
const btn = document.getElementById("scheduler-release-btn");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", () => {
|
||||
@@ -445,8 +443,8 @@
|
||||
}
|
||||
vchanStartSchedulerReleasePolling();
|
||||
vchanRenderSchedulerRelease();
|
||||
})();
|
||||
(function() {
|
||||
})();
|
||||
(function() {
|
||||
origRefreshFreqDisplay = vchanWindow.refreshFreqDisplay ?? null;
|
||||
vchanWindow.refreshFreqDisplay = function() {
|
||||
if (vchanIsOnVirtual()) {
|
||||
@@ -455,5 +453,4 @@
|
||||
}
|
||||
origRefreshFreqDisplay?.();
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/vdes.ts
|
||||
var vdesWindow = window;
|
||||
var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
var vdesStatus = document.getElementById("vdes-status");
|
||||
var vdesMessagesEl = document.getElementById("vdes-messages");
|
||||
var vdesFilterInput = document.getElementById("vdes-filter");
|
||||
var vdesBarOverlay = document.getElementById("vdes-bar-overlay");
|
||||
var vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
|
||||
var vdesFrameCountEl = document.getElementById("vdes-frame-count");
|
||||
var vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
|
||||
var VDES_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var vdesFilterText = "";
|
||||
var vdesMessageHistory = [];
|
||||
function currentVdesHistoryRetentionMs() {
|
||||
// src/plugins/vdes.ts
|
||||
var vdesWindow = window;
|
||||
var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
var vdesStatus = document.getElementById("vdes-status");
|
||||
var vdesMessagesEl = document.getElementById("vdes-messages");
|
||||
var vdesFilterInput = document.getElementById("vdes-filter");
|
||||
var vdesBarOverlay = document.getElementById("vdes-bar-overlay");
|
||||
var vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
|
||||
var vdesFrameCountEl = document.getElementById("vdes-frame-count");
|
||||
var vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
|
||||
var VDES_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
||||
var vdesFilterText = "";
|
||||
var vdesMessageHistory = [];
|
||||
function currentVdesHistoryRetentionMs() {
|
||||
return typeof vdesWindow.getDecodeHistoryRetentionMs === "function" ? vdesWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
|
||||
}
|
||||
function pruneVdesMessageHistory() {
|
||||
}
|
||||
function pruneVdesMessageHistory() {
|
||||
const cutoffMs = Date.now() - currentVdesHistoryRetentionMs();
|
||||
vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
|
||||
}
|
||||
function scheduleVdesUi(key, job) {
|
||||
}
|
||||
function scheduleVdesUi(key, job) {
|
||||
if (typeof vdesWindow.trxScheduleUiFrameJob === "function") {
|
||||
vdesWindow.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
function scheduleVdesHistoryRender() {
|
||||
}
|
||||
function scheduleVdesHistoryRender() {
|
||||
scheduleVdesUi("vdes-history", () => {
|
||||
renderVdesHistory();
|
||||
});
|
||||
}
|
||||
function scheduleVdesBarUpdate() {
|
||||
}
|
||||
function scheduleVdesBarUpdate() {
|
||||
scheduleVdesUi("vdes-bar", () => {
|
||||
updateVdesBar();
|
||||
});
|
||||
}
|
||||
function currentVdesCenterText() {
|
||||
}
|
||||
function currentVdesCenterText() {
|
||||
const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
|
||||
const hz = raw ? Number(raw) : 0;
|
||||
if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency";
|
||||
return `100 kHz @ ${(hz / 1e6).toFixed(3)} MHz`;
|
||||
}
|
||||
function vdesAgeText(tsMs) {
|
||||
}
|
||||
function vdesAgeText(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);
|
||||
@@ -53,12 +51,12 @@
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
function vdesHexPreview(rawBytes) {
|
||||
}
|
||||
function vdesHexPreview(rawBytes) {
|
||||
if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
|
||||
return rawBytes.slice(0, 20).map((value) => value.toString(16).padStart(2, "0")).join(" ").toUpperCase();
|
||||
}
|
||||
function updateVdesSummary() {
|
||||
}
|
||||
function updateVdesSummary() {
|
||||
pruneVdesMessageHistory();
|
||||
if (vdesChannelSummaryEl) {
|
||||
vdesChannelSummaryEl.textContent = currentVdesCenterText();
|
||||
@@ -71,16 +69,16 @@
|
||||
const latest = vdesMessageHistory[0];
|
||||
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
|
||||
}
|
||||
}
|
||||
function applyVdesFilterToRow(row) {
|
||||
}
|
||||
function applyVdesFilterToRow(row) {
|
||||
if (!vdesFilterText) {
|
||||
row.style.display = "";
|
||||
return;
|
||||
}
|
||||
const text = row.dataset.filterText || "";
|
||||
row.style.display = text.includes(vdesFilterText) ? "" : "none";
|
||||
}
|
||||
function renderVdesRow(msg) {
|
||||
}
|
||||
function renderVdesRow(msg) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "vdes-message";
|
||||
const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
||||
@@ -129,8 +127,8 @@
|
||||
row.innerHTML = `<div class="vdes-row-head"><span class="vdes-time">${ts}</span><span class="vdes-call">${escapeVdesHtml(title)}</span><span class="vdes-badge">${escapeVdesHtml(label)}</span>` + (labelText ? `<span class="vdes-badge">${escapeVdesHtml(labelText)}</span>` : "") + (linkText ? `<span class="vdes-badge">${escapeVdesHtml(linkText)}</span>` : "") + (srcText ? `<span class="vdes-badge">${escapeVdesHtml(srcText)}</span>` : "") + (dstText ? `<span class="vdes-badge">${escapeVdesHtml(dstText)}</span>` : "") + (syncText ? `<span class="vdes-badge">${escapeVdesHtml(syncText)}</span>` : "") + (phaseText ? `<span class="vdes-badge">${escapeVdesHtml(phaseText)}</span>` : "") + `<span class="vdes-badge">T${escapeVdesHtml(String(msg.message_type ?? "--"))}</span></div><div class="vdes-row-meta"><span>${escapeVdesHtml(currentVdesCenterText())}</span><span>${escapeVdesHtml(`${msg.bit_len || 0} bits`)}</span>` + (sessionText ? `<span>${escapeVdesHtml(sessionText)}</span>` : "") + (asmText ? `<span>${escapeVdesHtml(asmText)}</span>` : "") + (countText ? `<span>${escapeVdesHtml(countText)}</span>` : "") + (ackText ? `<span>${escapeVdesHtml(ackText)}</span>` : "") + (cqiText ? `<span>${escapeVdesHtml(cqiText)}</span>` : "") + (info ? `<span>${escapeVdesHtml(info)}</span>` : "") + (fecText ? `<span>${escapeVdesHtml(fecText)}</span>` : "") + `<span>${escapeVdesHtml(vdesAgeText(msg._tsMs))}</span></div><div class="vdes-row-detail">` + (previewText ? `<span>${escapeVdesHtml(previewText)}</span>` : "") + (previewText ? `<span>·</span>` : "") + `<span class="vdes-raw">${escapeVdesHtml(rawHex)}</span></div>`;
|
||||
applyVdesFilterToRow(row);
|
||||
return row;
|
||||
}
|
||||
function updateVdesBar() {
|
||||
}
|
||||
function updateVdesBar() {
|
||||
if (!vdesBarOverlay) return;
|
||||
updateVdesSummary();
|
||||
const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES";
|
||||
@@ -162,18 +160,18 @@
|
||||
}
|
||||
vdesBarOverlay.innerHTML = html;
|
||||
vdesBarOverlay.style.display = "flex";
|
||||
}
|
||||
vdesWindow.updateVdesBar = updateVdesBar;
|
||||
vdesWindow.clearVdesBar = function() {
|
||||
}
|
||||
vdesWindow.updateVdesBar = updateVdesBar;
|
||||
vdesWindow.clearVdesBar = function() {
|
||||
resetVdesHistoryView();
|
||||
};
|
||||
function resetVdesHistoryView() {
|
||||
};
|
||||
function resetVdesHistoryView() {
|
||||
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
||||
vdesMessageHistory = [];
|
||||
updateVdesBar();
|
||||
renderVdesHistory();
|
||||
}
|
||||
function renderVdesHistory() {
|
||||
}
|
||||
function renderVdesHistory() {
|
||||
pruneVdesMessageHistory();
|
||||
if (!vdesMessagesEl) {
|
||||
updateVdesSummary();
|
||||
@@ -185,8 +183,8 @@
|
||||
}
|
||||
vdesMessagesEl.replaceChildren(fragment);
|
||||
updateVdesSummary();
|
||||
}
|
||||
function addVdesMessage(msg) {
|
||||
}
|
||||
function addVdesMessage(msg) {
|
||||
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||
msg._tsMs = tsMs;
|
||||
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||
@@ -198,14 +196,14 @@
|
||||
pruneVdesMessageHistory();
|
||||
scheduleVdesBarUpdate();
|
||||
scheduleVdesHistoryRender();
|
||||
}
|
||||
function normalizeServerVdesMessage(msg) {
|
||||
}
|
||||
function normalizeServerVdesMessage(msg) {
|
||||
return {
|
||||
...msg,
|
||||
rig_id: msg.rig_id || null
|
||||
};
|
||||
}
|
||||
function onServerVdesBatch(messages) {
|
||||
}
|
||||
function onServerVdesBatch(messages) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||
const normalized = [];
|
||||
@@ -228,8 +226,8 @@
|
||||
pruneVdesMessageHistory();
|
||||
scheduleVdesBarUpdate();
|
||||
scheduleVdesHistoryRender();
|
||||
}
|
||||
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => {
|
||||
}
|
||||
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
@@ -239,33 +237,32 @@
|
||||
console.error("VDES history clear failed", e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
if (vdesFilterInput) {
|
||||
});
|
||||
if (vdesFilterInput) {
|
||||
vdesFilterInput.addEventListener("input", () => {
|
||||
vdesFilterText = vdesFilterInput.value.trim().toUpperCase();
|
||||
renderVdesHistory();
|
||||
});
|
||||
}
|
||||
function onServerVdes(msg) {
|
||||
}
|
||||
function onServerVdes(msg) {
|
||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||
const next = normalizeServerVdesMessage(msg);
|
||||
addVdesMessage(next);
|
||||
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
|
||||
vdesWindow.vdesMapAddPoint(next);
|
||||
}
|
||||
}
|
||||
function pruneVdesHistoryView() {
|
||||
}
|
||||
function pruneVdesHistoryView() {
|
||||
pruneVdesMessageHistory();
|
||||
updateVdesBar();
|
||||
renderVdesHistory();
|
||||
}
|
||||
updateVdesSummary();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
updateVdesSummary();
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
id: "vdes",
|
||||
onMessage: onServerVdes,
|
||||
onBatch: onServerVdesBatch,
|
||||
restore: onServerVdesBatch,
|
||||
reset: resetVdesHistoryView,
|
||||
prune: pruneVdesHistoryView
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/wefax.ts
|
||||
var wefaxWindow = window;
|
||||
var wefaxDom = {
|
||||
// src/plugins/wefax.ts
|
||||
var wefaxWindow = window;
|
||||
var wefaxDom = {
|
||||
status: document.getElementById("wefax-status"),
|
||||
liveView: document.getElementById("wefax-live-view"),
|
||||
historyView: document.getElementById("wefax-history-view"),
|
||||
@@ -18,34 +16,34 @@
|
||||
clearBtn: document.getElementById("wefax-clear-btn"),
|
||||
viewLiveBtn: document.getElementById("wefax-view-live"),
|
||||
viewHistoryBtn: document.getElementById("wefax-view-history")
|
||||
};
|
||||
var wefaxImageHistory = [];
|
||||
var WEFAX_MAX_IMAGES = 100;
|
||||
var wefaxLiveCtx = null;
|
||||
var wefaxLiveLineCount = 0;
|
||||
var wefaxLivePixelsPerLine = 1809;
|
||||
var wefaxActiveView = "live";
|
||||
var wefaxFilterText = "";
|
||||
function currentWefaxHistoryRetentionMs() {
|
||||
};
|
||||
var wefaxImageHistory = [];
|
||||
var WEFAX_MAX_IMAGES = 100;
|
||||
var wefaxLiveCtx = null;
|
||||
var wefaxLiveLineCount = 0;
|
||||
var wefaxLivePixelsPerLine = 1809;
|
||||
var wefaxActiveView = "live";
|
||||
var wefaxFilterText = "";
|
||||
function currentWefaxHistoryRetentionMs() {
|
||||
return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1e3;
|
||||
}
|
||||
function pruneWefaxHistory() {
|
||||
}
|
||||
function pruneWefaxHistory() {
|
||||
const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
||||
wefaxImageHistory = wefaxImageHistory.filter(function(m) {
|
||||
return (m._tsMs || 0) > cutoff;
|
||||
});
|
||||
}
|
||||
function escapeHtml(s) {
|
||||
}
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
function scheduleWefaxUi(key, job) {
|
||||
}
|
||||
function scheduleWefaxUi(key, job) {
|
||||
if (typeof wefaxWindow.trxScheduleUiFrameJob === "function") {
|
||||
wefaxWindow.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
function switchWefaxView(view) {
|
||||
}
|
||||
function switchWefaxView(view) {
|
||||
wefaxActiveView = view;
|
||||
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === "live" ? "" : "none";
|
||||
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === "history" ? "" : "none";
|
||||
@@ -55,14 +53,14 @@
|
||||
if (view === "live" && wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.classList.add("sat-view-active");
|
||||
if (view === "history" && wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.classList.add("sat-view-active");
|
||||
if (view === "history") renderWefaxHistoryTable();
|
||||
}
|
||||
if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener("click", function() {
|
||||
}
|
||||
if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener("click", function() {
|
||||
switchWefaxView("live");
|
||||
});
|
||||
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
|
||||
});
|
||||
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
|
||||
switchWefaxView("history");
|
||||
});
|
||||
function resetLiveCanvas(pixelsPerLine) {
|
||||
});
|
||||
function resetLiveCanvas(pixelsPerLine) {
|
||||
const canvas = wefaxDom.liveCanvas;
|
||||
if (!canvas) return;
|
||||
wefaxLivePixelsPerLine = pixelsPerLine;
|
||||
@@ -74,8 +72,8 @@
|
||||
wefaxLiveCtx.fillStyle = "#000";
|
||||
wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
|
||||
}
|
||||
function paintLine(lineBytes) {
|
||||
}
|
||||
function paintLine(lineBytes) {
|
||||
const canvas = wefaxDom.liveCanvas;
|
||||
if (!wefaxLiveCtx || !canvas) return;
|
||||
const y = wefaxLiveLineCount;
|
||||
@@ -99,8 +97,8 @@
|
||||
}
|
||||
wefaxLiveCtx.putImageData(imgData, 0, y);
|
||||
wefaxLiveLineCount++;
|
||||
}
|
||||
function renderWefaxLatestCard() {
|
||||
}
|
||||
function renderWefaxLatestCard() {
|
||||
if (!wefaxDom.liveLatest) return;
|
||||
if (wefaxImageHistory.length === 0) {
|
||||
wefaxDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
||||
@@ -125,8 +123,8 @@
|
||||
}
|
||||
html += "</div>";
|
||||
wefaxDom.liveLatest.innerHTML = html;
|
||||
}
|
||||
function getWefaxFilteredHistory() {
|
||||
}
|
||||
function getWefaxFilteredHistory() {
|
||||
let items = wefaxImageHistory;
|
||||
if (wefaxFilterText) {
|
||||
items = items.filter(function(i) {
|
||||
@@ -141,8 +139,8 @@
|
||||
const sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : "newest";
|
||||
if (sortVal === "oldest") items = items.slice().reverse();
|
||||
return items;
|
||||
}
|
||||
function renderWefaxHistoryRow(img) {
|
||||
}
|
||||
function renderWefaxHistoryRow(img) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-history-row";
|
||||
const ts = img._ts || "--";
|
||||
@@ -160,8 +158,8 @@
|
||||
"<span>" + link + "</span>"
|
||||
].join("");
|
||||
return row;
|
||||
}
|
||||
function renderWefaxHistoryTable() {
|
||||
}
|
||||
function renderWefaxHistoryTable() {
|
||||
if (!wefaxDom.historyList) return;
|
||||
pruneWefaxHistory();
|
||||
const items = getWefaxFilteredHistory();
|
||||
@@ -175,8 +173,8 @@
|
||||
const shown = items.length;
|
||||
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${String(total)} image${total === 1 ? "" : "s"}` : `${String(shown)} of ${String(total)} images`;
|
||||
}
|
||||
}
|
||||
function addWefaxImage(msg) {
|
||||
}
|
||||
function addWefaxImage(msg) {
|
||||
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||
msg._tsMs = tsMs;
|
||||
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||
@@ -204,8 +202,8 @@
|
||||
if (wefaxActiveView === "history") {
|
||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||
}
|
||||
}
|
||||
function onServerWefaxProgress(msg) {
|
||||
}
|
||||
function onServerWefaxProgress(msg) {
|
||||
if (msg.state && !msg.line_data) {
|
||||
if (wefaxDom.status) {
|
||||
wefaxDom.status.textContent = msg.state;
|
||||
@@ -229,16 +227,16 @@
|
||||
wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`;
|
||||
wefaxDom.status.style.color = "var(--text-accent)";
|
||||
}
|
||||
}
|
||||
function onServerWefax(msg) {
|
||||
}
|
||||
function onServerWefax(msg) {
|
||||
addWefaxImage(msg);
|
||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
||||
if (wefaxDom.status) {
|
||||
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
|
||||
wefaxDom.status.style.color = "";
|
||||
}
|
||||
}
|
||||
function restoreWefaxHistory(messages) {
|
||||
}
|
||||
function restoreWefaxHistory(messages) {
|
||||
if (!messages.length) return;
|
||||
for (const message of messages) {
|
||||
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
|
||||
@@ -255,13 +253,13 @@
|
||||
if (wefaxActiveView === "history") {
|
||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||
}
|
||||
}
|
||||
function pruneWefaxHistoryView() {
|
||||
}
|
||||
function pruneWefaxHistoryView() {
|
||||
pruneWefaxHistory();
|
||||
renderWefaxHistoryTable();
|
||||
renderWefaxLatestCard();
|
||||
}
|
||||
function resetWefaxHistoryView() {
|
||||
}
|
||||
function resetWefaxHistoryView() {
|
||||
wefaxImageHistory = [];
|
||||
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
|
||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
||||
@@ -273,27 +271,27 @@
|
||||
wefaxDom.status.textContent = "Idle";
|
||||
wefaxDom.status.style.color = "";
|
||||
}
|
||||
}
|
||||
if (wefaxDom.filterInput) {
|
||||
}
|
||||
if (wefaxDom.filterInput) {
|
||||
const filterInput = wefaxDom.filterInput;
|
||||
wefaxDom.filterInput.addEventListener("input", function() {
|
||||
wefaxFilterText = filterInput.value.trim().toUpperCase();
|
||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||
});
|
||||
}
|
||||
if (wefaxDom.sortSelect) {
|
||||
}
|
||||
if (wefaxDom.sortSelect) {
|
||||
wefaxDom.sortSelect.addEventListener("change", function() {
|
||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||
});
|
||||
}
|
||||
wefaxWindow.syncWefaxToggle = function(enabled) {
|
||||
}
|
||||
wefaxWindow.syncWefaxToggle = function(enabled) {
|
||||
if (!wefaxDom.toggleBtn) return;
|
||||
wefaxDom.toggleBtn.dataset.enabled = enabled ? "true" : "false";
|
||||
wefaxDom.toggleBtn.textContent = enabled ? "Disable WEFAX" : "Enable WEFAX";
|
||||
wefaxDom.toggleBtn.style.borderColor = enabled ? "#00d17f" : "";
|
||||
wefaxDom.toggleBtn.style.color = enabled ? "#00d17f" : "";
|
||||
};
|
||||
if (wefaxDom.toggleBtn) {
|
||||
};
|
||||
if (wefaxDom.toggleBtn) {
|
||||
const toggleButton = wefaxDom.toggleBtn;
|
||||
wefaxDom.toggleBtn.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
@@ -307,8 +305,8 @@
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
if (wefaxDom.clearBtn) {
|
||||
}
|
||||
if (wefaxDom.clearBtn) {
|
||||
wefaxDom.clearBtn.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -319,17 +317,16 @@
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
renderWefaxLatestCard();
|
||||
wefaxWindow.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
renderWefaxLatestCard();
|
||||
wefaxWindow.trxPluginRuntime.registerDecoder({
|
||||
id: "wefax",
|
||||
onMessage: onServerWefax,
|
||||
restore: restoreWefaxHistory,
|
||||
prune: pruneWefaxHistoryView,
|
||||
reset: resetWefaxHistoryView
|
||||
});
|
||||
wefaxWindow.trxPluginRuntime.registerDecoder({
|
||||
});
|
||||
wefaxWindow.trxPluginRuntime.registerDecoder({
|
||||
id: "wefax_progress",
|
||||
onMessage: onServerWefaxProgress
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/wspr.ts
|
||||
var wsprWindow = window;
|
||||
var wsprStatus = document.getElementById("wspr-status");
|
||||
var wsprPeriodEl = document.getElementById("wspr-period");
|
||||
var wsprMessagesEl = document.getElementById("wspr-messages");
|
||||
var wsprFilterInput = document.getElementById("wspr-filter");
|
||||
var WSPR_PERIOD_SECONDS = 120;
|
||||
var wsprFilterText = "";
|
||||
var wsprMessageHistory = [];
|
||||
function finiteNumber(value) {
|
||||
// src/plugins/wspr.ts
|
||||
var wsprWindow = window;
|
||||
var wsprStatus = document.getElementById("wspr-status");
|
||||
var wsprPeriodEl = document.getElementById("wspr-period");
|
||||
var wsprMessagesEl = document.getElementById("wspr-messages");
|
||||
var wsprFilterInput = document.getElementById("wspr-filter");
|
||||
var WSPR_PERIOD_SECONDS = 120;
|
||||
var wsprFilterText = "";
|
||||
var wsprMessageHistory = [];
|
||||
function finiteNumber(value) {
|
||||
const number = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
function currentWsprHistoryRetentionMs() {
|
||||
}
|
||||
function currentWsprHistoryRetentionMs() {
|
||||
return typeof wsprWindow.getDecodeHistoryRetentionMs === "function" ? wsprWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
|
||||
}
|
||||
function pruneWsprMessageHistory() {
|
||||
}
|
||||
function pruneWsprMessageHistory() {
|
||||
const cutoffMs = Date.now() - currentWsprHistoryRetentionMs();
|
||||
wsprMessageHistory = wsprMessageHistory.filter((msg) => Number(msg._tsMs ?? msg.ts_ms) >= cutoffMs);
|
||||
}
|
||||
function scheduleWsprHistoryRender() {
|
||||
}
|
||||
function scheduleWsprHistoryRender() {
|
||||
if (typeof wsprWindow.trxScheduleUiFrameJob === "function") {
|
||||
wsprWindow.trxScheduleUiFrameJob("wspr-history", () => {
|
||||
renderWsprHistory();
|
||||
@@ -28,22 +26,22 @@
|
||||
return;
|
||||
}
|
||||
renderWsprHistory();
|
||||
}
|
||||
function fmtWsprTime(tsMs) {
|
||||
}
|
||||
function fmtWsprTime(tsMs) {
|
||||
if (!tsMs) return "--:--:--";
|
||||
return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
function updateWsprPeriodTimer() {
|
||||
}
|
||||
function updateWsprPeriodTimer() {
|
||||
if (!wsprPeriodEl) return;
|
||||
const nowSec = Math.floor(Date.now() / 1e3);
|
||||
const remaining = WSPR_PERIOD_SECONDS - nowSec % WSPR_PERIOD_SECONDS;
|
||||
const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
|
||||
const ss = String(remaining % 60).padStart(2, "0");
|
||||
wsprPeriodEl.textContent = `Next slot ${mm}:${ss}`;
|
||||
}
|
||||
updateWsprPeriodTimer();
|
||||
setInterval(updateWsprPeriodTimer, 500);
|
||||
function renderWsprRow(msg) {
|
||||
}
|
||||
updateWsprPeriodTimer();
|
||||
setInterval(updateWsprPeriodTimer, 500);
|
||||
function renderWsprRow(msg) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
row.dataset.decoder = "wspr";
|
||||
@@ -58,8 +56,8 @@
|
||||
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(msg.ts_ms)}</span><span class="ft8-snr">${snr?.toFixed(1) ?? "--"}</span><span class="ft8-dt">${delta?.toFixed(2) ?? "--"}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderWsprMessage(message)}</span>`;
|
||||
applyWsprFilterToRow(row);
|
||||
return row;
|
||||
}
|
||||
function renderWsprHistory() {
|
||||
}
|
||||
function renderWsprHistory() {
|
||||
pruneWsprMessageHistory();
|
||||
if (!wsprMessagesEl) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
@@ -68,14 +66,14 @@
|
||||
if (message) fragment.appendChild(renderWsprRow(message));
|
||||
}
|
||||
wsprMessagesEl.replaceChildren(fragment);
|
||||
}
|
||||
function addWsprMessage(msg) {
|
||||
}
|
||||
function addWsprMessage(msg) {
|
||||
msg._tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||
wsprMessageHistory.unshift(msg);
|
||||
pruneWsprMessageHistory();
|
||||
scheduleWsprHistoryRender();
|
||||
}
|
||||
function normalizeServerWsprMessage(msg) {
|
||||
}
|
||||
function normalizeServerWsprMessage(msg) {
|
||||
const raw = msg.message ?? "";
|
||||
const grids = extractAllGrids(raw);
|
||||
const station = extractLikelyCallsign(raw);
|
||||
@@ -96,8 +94,8 @@
|
||||
message: raw
|
||||
}
|
||||
};
|
||||
}
|
||||
function onServerWsprBatch(messages) {
|
||||
}
|
||||
function onServerWsprBatch(messages) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
if (wsprStatus) wsprStatus.textContent = "Receiving";
|
||||
const normalized = [];
|
||||
@@ -116,15 +114,15 @@
|
||||
wsprMessageHistory = normalized.concat(wsprMessageHistory);
|
||||
pruneWsprMessageHistory();
|
||||
scheduleWsprHistoryRender();
|
||||
}
|
||||
function pruneWsprHistoryView() {
|
||||
}
|
||||
function pruneWsprHistoryView() {
|
||||
pruneWsprMessageHistory();
|
||||
renderWsprHistory();
|
||||
}
|
||||
function escapeWsprHtml(input) {
|
||||
}
|
||||
function escapeWsprHtml(input) {
|
||||
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
function renderWsprMessage(message) {
|
||||
}
|
||||
function renderWsprMessage(message) {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
while (i < message.length) {
|
||||
@@ -146,8 +144,8 @@
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function extractAllGrids(message) {
|
||||
}
|
||||
function extractAllGrids(message) {
|
||||
const out = [];
|
||||
const seen = /* @__PURE__ */ new Set();
|
||||
const parts = message.toUpperCase().split(/[^A-Z0-9]+/);
|
||||
@@ -159,8 +157,8 @@
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function extractLikelyCallsign(message) {
|
||||
}
|
||||
function extractLikelyCallsign(message) {
|
||||
const parts = message.toUpperCase().split(/[^A-Z0-9/]+/);
|
||||
for (const token of parts) {
|
||||
if (!token) continue;
|
||||
@@ -170,19 +168,19 @@
|
||||
if (/^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token)) return token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isFtxFarewellToken(token) {
|
||||
}
|
||||
function isFtxFarewellToken(token) {
|
||||
const normalized = token.trim().toUpperCase();
|
||||
return normalized === "RR73" || normalized === "73" || normalized === "RR";
|
||||
}
|
||||
function isMaidenheadGridToken(token) {
|
||||
}
|
||||
function isMaidenheadGridToken(token) {
|
||||
const normalized = token.trim().toUpperCase();
|
||||
return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized);
|
||||
}
|
||||
function isAlphaNum(ch) {
|
||||
}
|
||||
function isAlphaNum(ch) {
|
||||
return ch !== void 0 && /[A-Za-z0-9]/.test(ch);
|
||||
}
|
||||
function activateWsprHistoryLocator(target) {
|
||||
}
|
||||
function activateWsprHistoryLocator(target) {
|
||||
if (!(target instanceof Element)) return false;
|
||||
const locatorEl = target.closest(".ft8-locator[data-locator-grid]");
|
||||
if (!locatorEl) return false;
|
||||
@@ -192,28 +190,28 @@
|
||||
wsprWindow.navigateToMapLocator(grid, "wspr");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function applyWsprFilterToRow(row) {
|
||||
}
|
||||
function applyWsprFilterToRow(row) {
|
||||
if (!wsprFilterText) {
|
||||
row.style.display = "";
|
||||
return;
|
||||
}
|
||||
const message = row.dataset.message || "";
|
||||
row.style.display = message.includes(wsprFilterText) ? "" : "none";
|
||||
}
|
||||
function resetWsprHistoryView() {
|
||||
}
|
||||
function resetWsprHistoryView() {
|
||||
if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
|
||||
wsprMessageHistory = [];
|
||||
renderWsprHistory();
|
||||
if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
|
||||
}
|
||||
if (wsprFilterInput) {
|
||||
}
|
||||
if (wsprFilterInput) {
|
||||
wsprFilterInput.addEventListener("input", () => {
|
||||
wsprFilterText = wsprFilterInput.value.trim().toUpperCase();
|
||||
renderWsprHistory();
|
||||
});
|
||||
}
|
||||
if (wsprMessagesEl) {
|
||||
}
|
||||
if (wsprMessagesEl) {
|
||||
wsprMessagesEl.addEventListener("click", (event) => {
|
||||
if (!activateWsprHistoryLocator(event.target)) return;
|
||||
event.preventDefault();
|
||||
@@ -225,9 +223,9 @@
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
var wsprDecodeToggleBtn = document.getElementById("wspr-decode-toggle-btn");
|
||||
wsprDecodeToggleBtn?.addEventListener("click", () => {
|
||||
}
|
||||
var wsprDecodeToggleBtn = document.getElementById("wspr-decode-toggle-btn");
|
||||
wsprDecodeToggleBtn?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await wsprWindow.takeSchedulerControlForDecoderDisable?.(wsprDecodeToggleBtn);
|
||||
@@ -236,8 +234,8 @@
|
||||
console.error("WSPR toggle failed", error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
document.getElementById("settings-clear-wspr-history")?.addEventListener("click", () => {
|
||||
});
|
||||
document.getElementById("settings-clear-wspr-history")?.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||
try {
|
||||
@@ -247,8 +245,8 @@
|
||||
console.error("WSPR history clear failed", error);
|
||||
}
|
||||
})();
|
||||
});
|
||||
function onServerWspr(msg) {
|
||||
});
|
||||
function onServerWspr(msg) {
|
||||
if (wsprStatus) wsprStatus.textContent = "Receiving";
|
||||
const next = normalizeServerWsprMessage(msg);
|
||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
||||
@@ -258,13 +256,12 @@
|
||||
});
|
||||
}
|
||||
addWsprMessage(next.history);
|
||||
}
|
||||
wsprWindow.trxPluginRuntime.registerDecoder({
|
||||
}
|
||||
wsprWindow.trxPluginRuntime.registerDecoder({
|
||||
id: "wspr",
|
||||
onMessage: onServerWspr,
|
||||
onBatch: onServerWsprBatch,
|
||||
restore: onServerWsprBatch,
|
||||
prune: pruneWsprHistoryView,
|
||||
reset: resetWsprHistoryView
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -67,7 +67,10 @@ await build({
|
||||
},
|
||||
outdir: outputDir,
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
format: "esm",
|
||||
splitting: true,
|
||||
entryNames: "[name]",
|
||||
chunkNames: "chunk-[hash]",
|
||||
platform: "browser",
|
||||
target: "es2022",
|
||||
sourcemap: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { bundleEntry } from "./bundle-entry.mjs";
|
||||
|
||||
test("APRS entry normalizes positioned packets without remote symbol assets", async () => {
|
||||
const forwarded = [];
|
||||
@@ -26,7 +27,7 @@ test("APRS entry normalizes positioned packets without remote symbol assets", as
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/aprs.js", import.meta.url), "utf8");
|
||||
const source = await bundleEntry(new URL("../src/plugins/aprs.ts", import.meta.url));
|
||||
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import { build } from "esbuild";
|
||||
|
||||
export async function bundleEntry(entryUrl) {
|
||||
const result = await build({
|
||||
entryPoints: [entryUrl.pathname],
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
platform: "browser",
|
||||
target: "es2022",
|
||||
write: false,
|
||||
});
|
||||
const output = result.outputFiles[0];
|
||||
if (!output) throw new Error(`No bundle output for ${entryUrl.pathname}`);
|
||||
return output.text;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { bundleEntry } from "./bundle-entry.mjs";
|
||||
|
||||
test("FT2 entry normalizes audio offsets and registers typed callbacks", async () => {
|
||||
let barRenderer;
|
||||
@@ -30,7 +31,7 @@ test("FT2 entry normalizes audio offsets and registers typed callbacks", async (
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/ft2.js", import.meta.url), "utf8");
|
||||
const source = await bundleEntry(new URL("../src/plugins/ft2.ts", import.meta.url));
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
@@ -62,7 +63,7 @@ test("FT8 entry installs shared parsing without relying on script globals", asyn
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/ft8.js", import.meta.url), "utf8");
|
||||
const source = await bundleEntry(new URL("../src/plugins/ft8.ts", import.meta.url));
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { bundleEntry } from "./bundle-entry.mjs";
|
||||
|
||||
test("HF APRS entry uses shared typed normalization and local symbols", async () => {
|
||||
const window = { trxUi: { confirm: async () => true } };
|
||||
@@ -22,7 +23,7 @@ test("HF APRS entry uses shared typed normalization and local symbols", async ()
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/hf-aprs.js", import.meta.url), "utf8");
|
||||
const source = await bundleEntry(new URL("../src/plugins/hf-aprs.ts", import.meta.url));
|
||||
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
Reference in New Issue
Block a user