build: enable frontend module code splitting
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/ais.ts
|
// src/plugins/ais.ts
|
||||||
var aisWindow = window;
|
var aisWindow = window;
|
||||||
var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||||
@@ -331,4 +329,3 @@
|
|||||||
reset: resetAisHistoryView,
|
reset: resetAisHistoryView,
|
||||||
prune: pruneAisHistoryView
|
prune: pruneAisHistoryView
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,106 +1,13 @@
|
|||||||
"use strict";
|
import {
|
||||||
(() => {
|
aprsAgeText,
|
||||||
// src/plugins/aprs-shared.ts
|
aprsCategoryLabel,
|
||||||
function aprsPacketCategory(packet) {
|
aprsHexBytes,
|
||||||
const type = (packet.type ?? "").toLowerCase();
|
aprsPacketCategory,
|
||||||
const info = (packet.info ?? "").toLowerCase();
|
collapseAprsDuplicates,
|
||||||
if (packet.lat != null && packet.lon != null || type.includes("position")) return "position";
|
normalizeAprsPacket,
|
||||||
if (type.includes("message") || info.startsWith(":")) return "message";
|
renderAprsInfo,
|
||||||
if (type.includes("weather") || info.startsWith("_")) return "weather";
|
renderLocalAprsSymbol
|
||||||
if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
|
} from "./chunk-M2I6DH4X.js";
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/plugins/aprs.ts
|
// src/plugins/aprs.ts
|
||||||
var aprsWindow = window;
|
var aprsWindow = window;
|
||||||
@@ -405,4 +312,3 @@
|
|||||||
reset: resetAprsHistoryView,
|
reset: resetAprsHistoryView,
|
||||||
prune: pruneAprsHistoryView
|
prune: pruneAprsHistoryView
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|||||||
-3
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/background-decode.ts
|
// src/plugins/background-decode.ts
|
||||||
var bgdWindow = window;
|
var bgdWindow = window;
|
||||||
(function() {
|
(function() {
|
||||||
@@ -362,4 +360,3 @@
|
|||||||
bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
|
bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
|
||||||
bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
|
bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
|
||||||
})();
|
})();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/bookmarks.ts
|
// src/plugins/bookmarks.ts
|
||||||
var bridge = window;
|
var bridge = window;
|
||||||
function bmEl(id) {
|
function bmEl(id) {
|
||||||
@@ -676,4 +674,3 @@
|
|||||||
});
|
});
|
||||||
void bmFetch("");
|
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,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/cw.ts
|
// src/plugins/cw.ts
|
||||||
var cwWindow = window;
|
var cwWindow = window;
|
||||||
var cwStatusEl = document.getElementById("cw-status");
|
var cwStatusEl = document.getElementById("cw-status");
|
||||||
@@ -400,4 +398,3 @@
|
|||||||
updateCwBar();
|
updateCwBar();
|
||||||
ensureCwToneCanvasResolution();
|
ensureCwToneCanvasResolution();
|
||||||
drawCwTonePicker();
|
drawCwTonePicker();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,227 +1,6 @@
|
|||||||
"use strict";
|
import {
|
||||||
(() => {
|
initializeFtxDecoder
|
||||||
// src/plugins/ftx-family.ts
|
} from "./chunk-SGMG5LG2.js";
|
||||||
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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/plugins/ft2.ts
|
// src/plugins/ft2.ts
|
||||||
initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
|
initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,227 +1,6 @@
|
|||||||
"use strict";
|
import {
|
||||||
(() => {
|
initializeFtxDecoder
|
||||||
// src/plugins/ftx-family.ts
|
} from "./chunk-SGMG5LG2.js";
|
||||||
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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/plugins/ft4.ts
|
// src/plugins/ft4.ts
|
||||||
initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
|
initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,271 +1,10 @@
|
|||||||
"use strict";
|
import {
|
||||||
(() => {
|
initializeFt8FamilyBar,
|
||||||
// src/plugins/ftx-family.ts
|
initializeFtxDecoder,
|
||||||
var bridge = window;
|
installFtxCompatibilityHelpers
|
||||||
function finiteNumber(value) {
|
} from "./chunk-SGMG5LG2.js";
|
||||||
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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/plugins/ft8.ts
|
// src/plugins/ft8.ts
|
||||||
installFtxCompatibilityHelpers();
|
installFtxCompatibilityHelpers();
|
||||||
initializeFt8FamilyBar();
|
initializeFt8FamilyBar();
|
||||||
initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 });
|
initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 });
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,106 +1,13 @@
|
|||||||
"use strict";
|
import {
|
||||||
(() => {
|
aprsAgeText,
|
||||||
// src/plugins/aprs-shared.ts
|
aprsCategoryLabel,
|
||||||
function aprsPacketCategory(packet) {
|
aprsHexBytes,
|
||||||
const type = (packet.type ?? "").toLowerCase();
|
aprsPacketCategory,
|
||||||
const info = (packet.info ?? "").toLowerCase();
|
collapseAprsDuplicates,
|
||||||
if (packet.lat != null && packet.lon != null || type.includes("position")) return "position";
|
normalizeAprsPacket,
|
||||||
if (type.includes("message") || info.startsWith(":")) return "message";
|
renderAprsInfo,
|
||||||
if (type.includes("weather") || info.startsWith("_")) return "weather";
|
renderLocalAprsSymbol
|
||||||
if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
|
} from "./chunk-M2I6DH4X.js";
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/plugins/hf-aprs.ts
|
// src/plugins/hf-aprs.ts
|
||||||
var hfAprsWindow = window;
|
var hfAprsWindow = window;
|
||||||
@@ -356,4 +263,3 @@
|
|||||||
reset: resetHfAprsHistoryView,
|
reset: resetHfAprsHistoryView,
|
||||||
prune: pruneHfAprsHistoryView
|
prune: pruneHfAprsHistoryView
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|||||||
-3
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/leaflet-ais-tracksymbol.ts
|
// src/leaflet-ais-tracksymbol.ts
|
||||||
(function() {
|
(function() {
|
||||||
const leaflet = globalThis.L;
|
const leaflet = globalThis.L;
|
||||||
@@ -95,4 +93,3 @@
|
|||||||
return new Constructor(latlng, options);
|
return new Constructor(latlng, options);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/map-core.ts
|
// src/map-core.ts
|
||||||
function mapEl(id) {
|
function mapEl(id) {
|
||||||
const element = document.querySelector(`#${CSS.escape(id)}`);
|
const element = document.querySelector(`#${CSS.escape(id)}`);
|
||||||
@@ -3111,4 +3109,3 @@
|
|||||||
};
|
};
|
||||||
autoInitIfVisible();
|
autoInitIfVisible();
|
||||||
})();
|
})();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/sat-scheduler.ts
|
// src/plugins/sat-scheduler.ts
|
||||||
var satSchedulerWindow = window;
|
var satSchedulerWindow = window;
|
||||||
(function() {
|
(function() {
|
||||||
@@ -263,4 +261,3 @@
|
|||||||
renderSection();
|
renderSection();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/sat.ts
|
// src/plugins/sat.ts
|
||||||
var satWindow = window;
|
var satWindow = window;
|
||||||
var satDom = {
|
var satDom = {
|
||||||
@@ -479,4 +477,3 @@
|
|||||||
};
|
};
|
||||||
renderSatLatestCard();
|
renderSatLatestCard();
|
||||||
renderSatHistoryTable();
|
renderSatHistoryTable();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/scheduler.ts
|
// src/plugins/scheduler.ts
|
||||||
var schedulerWindow = window;
|
var schedulerWindow = window;
|
||||||
var wiredElements = /* @__PURE__ */ new WeakSet();
|
var wiredElements = /* @__PURE__ */ new WeakSet();
|
||||||
@@ -1220,4 +1218,3 @@
|
|||||||
wireSchedulerEvents();
|
wireSchedulerEvents();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/vchan.ts
|
// src/plugins/vchan.ts
|
||||||
var vchanWindow = window;
|
var vchanWindow = window;
|
||||||
var vchanSessionId = null;
|
var vchanSessionId = null;
|
||||||
@@ -456,4 +454,3 @@
|
|||||||
origRefreshFreqDisplay?.();
|
origRefreshFreqDisplay?.();
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/vdes.ts
|
// src/plugins/vdes.ts
|
||||||
var vdesWindow = window;
|
var vdesWindow = window;
|
||||||
var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||||
@@ -268,4 +266,3 @@
|
|||||||
reset: resetVdesHistoryView,
|
reset: resetVdesHistoryView,
|
||||||
prune: pruneVdesHistoryView
|
prune: pruneVdesHistoryView
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/wefax.ts
|
// src/plugins/wefax.ts
|
||||||
var wefaxWindow = window;
|
var wefaxWindow = window;
|
||||||
var wefaxDom = {
|
var wefaxDom = {
|
||||||
@@ -332,4 +330,3 @@
|
|||||||
id: "wefax_progress",
|
id: "wefax_progress",
|
||||||
onMessage: onServerWefaxProgress
|
onMessage: onServerWefaxProgress
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use strict";
|
|
||||||
(() => {
|
|
||||||
// src/plugins/wspr.ts
|
// src/plugins/wspr.ts
|
||||||
var wsprWindow = window;
|
var wsprWindow = window;
|
||||||
var wsprStatus = document.getElementById("wspr-status");
|
var wsprStatus = document.getElementById("wspr-status");
|
||||||
@@ -267,4 +265,3 @@
|
|||||||
prune: pruneWsprHistoryView,
|
prune: pruneWsprHistoryView,
|
||||||
reset: resetWsprHistoryView
|
reset: resetWsprHistoryView
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ await build({
|
|||||||
},
|
},
|
||||||
outdir: outputDir,
|
outdir: outputDir,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "iife",
|
format: "esm",
|
||||||
|
splitting: true,
|
||||||
|
entryNames: "[name]",
|
||||||
|
chunkNames: "chunk-[hash]",
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
target: "es2022",
|
target: "es2022",
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import vm from "node:vm";
|
import vm from "node:vm";
|
||||||
|
import { bundleEntry } from "./bundle-entry.mjs";
|
||||||
|
|
||||||
test("APRS entry normalizes positioned packets without remote symbol assets", async () => {
|
test("APRS entry normalizes positioned packets without remote symbol assets", async () => {
|
||||||
const forwarded = [];
|
const forwarded = [];
|
||||||
@@ -26,7 +27,7 @@ test("APRS entry normalizes positioned packets without remote symbol assets", as
|
|||||||
console,
|
console,
|
||||||
});
|
});
|
||||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
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);
|
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||||
new vm.Script(runtime).runInContext(context);
|
new vm.Script(runtime).runInContext(context);
|
||||||
new vm.Script(source).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 { readFile } from "node:fs/promises";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import vm from "node:vm";
|
import vm from "node:vm";
|
||||||
|
import { bundleEntry } from "./bundle-entry.mjs";
|
||||||
|
|
||||||
test("FT2 entry normalizes audio offsets and registers typed callbacks", async () => {
|
test("FT2 entry normalizes audio offsets and registers typed callbacks", async () => {
|
||||||
let barRenderer;
|
let barRenderer;
|
||||||
@@ -30,7 +31,7 @@ test("FT2 entry normalizes audio offsets and registers typed callbacks", async (
|
|||||||
console,
|
console,
|
||||||
});
|
});
|
||||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
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(runtime).runInContext(context);
|
||||||
new vm.Script(source).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,
|
console,
|
||||||
});
|
});
|
||||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
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(runtime).runInContext(context);
|
||||||
new vm.Script(source).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 { readFile } from "node:fs/promises";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import vm from "node:vm";
|
import vm from "node:vm";
|
||||||
|
import { bundleEntry } from "./bundle-entry.mjs";
|
||||||
|
|
||||||
test("HF APRS entry uses shared typed normalization and local symbols", async () => {
|
test("HF APRS entry uses shared typed normalization and local symbols", async () => {
|
||||||
const window = { trxUi: { confirm: async () => true } };
|
const window = { trxUi: { confirm: async () => true } };
|
||||||
@@ -22,7 +23,7 @@ test("HF APRS entry uses shared typed normalization and local symbols", async ()
|
|||||||
console,
|
console,
|
||||||
});
|
});
|
||||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
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);
|
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||||
new vm.Script(runtime).runInContext(context);
|
new vm.Script(runtime).runInContext(context);
|
||||||
new vm.Script(source).runInContext(context);
|
new vm.Script(source).runInContext(context);
|
||||||
|
|||||||
Reference in New Issue
Block a user