refactor: route all decoders through plugin runtime

This commit is contained in:
sjg
2026-08-01 13:06:38 +02:00
parent 508cf2ba87
commit 9fef0ebc7b
19 changed files with 255 additions and 319 deletions
@@ -488,12 +488,9 @@ function currentDecodeHistoryRetentionMs() {
} }
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs; window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
window.applyDecodeHistoryRetention = function() { window.applyDecodeHistoryRetention = function() {
window.trxPluginRuntime.prune("aprs"); for (const decoder of ["aprs", "hf_aprs", "ais", "vdes", "ft8", "ft4", "ft2", "wspr", "wefax"]) {
window.trxPluginRuntime.prune("hf_aprs"); window.trxPluginRuntime.prune(decoder);
window.trxPluginRuntime.prune("ais"); }
window.trxPluginRuntime.prune("vdes");
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
}; };
function syncTopBarAccess() { function syncTopBarAccess() {
const loggedOut = authEnabled && !authRole; const loggedOut = authEnabled && !authRole;
@@ -5857,18 +5854,7 @@ function updateDecodeStatus(text) {
} }
} }
function dispatchDecodeMessage(msg, skipStats) { function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") { if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
window.trxPluginRuntime.dispatch(msg.type, msg);
}
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg);
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg);
if (msg.type === "lrpt_progress" && window.onServerLrptProgress) window.onServerLrptProgress(msg);
if (msg.type === "wefax" && window.onServerWefax) window.onServerWefax(msg);
if (msg.type === "wefax_progress" && window.onServerWefaxProgress) window.onServerWefaxProgress(msg);
if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") { if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") {
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
window.trx.modules.map?.scheduleStatsRender(); window.trx.modules.map?.scheduleStatsRender();
@@ -5884,27 +5870,9 @@ function dispatchDecodeBatch(batch) {
window.trx.modules.map?.scheduleStatsRender(); window.trx.modules.map?.scheduleStatsRender();
const type = String(batch[0]?.type || ""); const type = String(batch[0]?.type || "");
const uniformType = batch.every((msg) => String(msg?.type || "") === type); const uniformType = batch.every((msg) => String(msg?.type || "") === type);
if (uniformType) { if (uniformType && type) {
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") { window.trxPluginRuntime.dispatchBatch(type, batch);
window.trxPluginRuntime.dispatchBatch(type, batch); return;
return;
}
if (type === "ft8" && window.onServerFt8Batch) {
window.onServerFt8Batch(batch);
return;
}
if (type === "ft4" && window.onServerFt4Batch) {
window.onServerFt4Batch(batch);
return;
}
if (type === "ft2" && window.onServerFt2Batch) {
window.onServerFt2Batch(batch);
return;
}
if (type === "wspr" && window.onServerWsprBatch) {
window.onServerWsprBatch(batch);
return;
}
} }
for (const msg of batch) { for (const msg of batch) {
dispatchDecodeMessage(msg, true); dispatchDecodeMessage(msg, true);
@@ -5954,34 +5922,7 @@ function restoreDecodeHistoryGroup(kind, messages) {
} }
window.trx.modules.map?.scheduleStatsRender(); window.trx.modules.map?.scheduleStatsRender();
} }
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") { window.trxPluginRuntime.restore(kind, messages);
window.trxPluginRuntime.restore(kind, messages);
return;
}
if (kind === "cw" && window.restoreCwHistory) {
window.restoreCwHistory(messages);
return;
}
if (kind === "ft8" && window.restoreFt8History) {
window.restoreFt8History(messages);
return;
}
if (kind === "ft4" && window.restoreFt4History) {
window.restoreFt4History(messages);
return;
}
if (kind === "ft2" && window.restoreFt2History) {
window.restoreFt2History(messages);
return;
}
if (kind === "wspr" && window.restoreWsprHistory) {
window.restoreWsprHistory(messages);
return;
}
if (kind === "wefax" && window.restoreWefaxHistory) {
window.restoreWefaxHistory(messages);
return;
}
} }
function connectDecode() { function connectDecode() {
if (decodeSource) { if (decodeSource) {
@@ -5991,14 +5932,7 @@ function connectDecode() {
decodeHistoryReplayActive = false; decodeHistoryReplayActive = false;
decodeMapSyncPending = false; decodeMapSyncPending = false;
window.trxPluginRuntime.clearQueued(); window.trxPluginRuntime.clearQueued();
window.trxPluginRuntime.reset("ais"); window.trxPluginRuntime.resetAll();
window.trxPluginRuntime.reset("vdes");
window.trxPluginRuntime.reset("aprs");
window.trxPluginRuntime.reset("hf_aprs");
if (window.resetCwHistoryView) window.resetCwHistoryView();
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
let historySettled = false; let historySettled = false;
let historyWorkerDone = false; let historyWorkerDone = false;
let historyFallbackStarted = false; let historyFallbackStarted = false;
@@ -82,7 +82,7 @@
} }
cwWindow.updateCwBar = updateCwBar; cwWindow.updateCwBar = updateCwBar;
cwWindow.clearCwBar = function() { cwWindow.clearCwBar = function() {
cwWindow.resetCwHistoryView?.(); resetCwHistoryView();
}; };
cwWindow.closeCwBar = function() { cwWindow.closeCwBar = function() {
cwBarDismissedAtMs = Date.now(); cwBarDismissedAtMs = Date.now();
@@ -302,26 +302,26 @@
void setCwTone(tone); void setCwTone(tone);
}); });
} }
cwWindow.resetCwHistoryView = function() { function resetCwHistoryView() {
if (cwOutputEl) cwOutputEl.innerHTML = ""; if (cwOutputEl) cwOutputEl.innerHTML = "";
cwLastAppendTime = 0; cwLastAppendTime = 0;
cwBarHistory = []; cwBarHistory = [];
cwBarCurrentLine = null; cwBarCurrentLine = null;
updateCwBar(); updateCwBar();
drawCwTonePicker(); drawCwTonePicker();
}; }
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => { document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
void (async () => { void (async () => {
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return; if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try { try {
await cwWindow.postPath?.("/clear_cw_decode"); await cwWindow.postPath?.("/clear_cw_decode");
cwWindow.resetCwHistoryView?.(); resetCwHistoryView();
} catch (error) { } catch (error) {
console.error("CW history clear failed", error); console.error("CW history clear failed", error);
} }
})(); })();
}); });
cwWindow.onServerCw = function(evt) { function onServerCw(evt) {
if (cwStatusEl) cwStatusEl.textContent = "Receiving"; if (cwStatusEl) cwStatusEl.textContent = "Receiving";
if (evt.text && cwOutputEl) { if (evt.text && cwOutputEl) {
const now = Date.now(); const now = Date.now();
@@ -375,14 +375,20 @@
cwTonePickerRaf = null; cwTonePickerRaf = null;
drawCwTonePicker(); drawCwTonePicker();
}); });
}; }
cwWindow.restoreCwHistory = function(events) { function restoreCwHistory(events) {
if (!Array.isArray(events) || events.length === 0) return; if (!Array.isArray(events) || events.length === 0) return;
if (cwStatusEl) cwStatusEl.textContent = "Receiving"; if (cwStatusEl) cwStatusEl.textContent = "Receiving";
for (const evt of events) { for (const evt of events) {
cwWindow.onServerCw?.(evt); onServerCw(evt);
} }
}; }
cwWindow.trxPluginRuntime.registerDecoder({
id: "cw",
onMessage: onServerCw,
restore: restoreCwHistory,
reset: resetCwHistoryView
});
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() { cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
ensureCwToneCanvasResolution(); ensureCwToneCanvasResolution();
drawCwTonePicker(); drawCwTonePicker();
@@ -167,19 +167,19 @@
} }
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html }; return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
}; };
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; bridge.trxPluginRuntime.registerDecoder({
bridge[`onServer${capitalized}Batch`] = (messages) => { id,
if (Array.isArray(messages)) receiveBatch(messages); onMessage: (message) => {
}; receiveBatch([message]);
bridge[`restore${capitalized}History`] = receiveBatch; },
bridge[`prune${capitalized}HistoryView`] = () => { onBatch: receiveBatch,
prune(); restore: receiveBatch,
render(); prune: () => {
}; prune();
bridge[`reset${capitalized}HistoryView`] = reset; render();
bridge[`onServer${capitalized}`] = (message) => { },
receiveBatch([message]); reset
}; });
bridge.registerFt8FamilyBarRenderer?.(id, barFrames); bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => { const updatePeriod = () => {
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
@@ -167,19 +167,19 @@
} }
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html }; return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
}; };
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; bridge.trxPluginRuntime.registerDecoder({
bridge[`onServer${capitalized}Batch`] = (messages) => { id,
if (Array.isArray(messages)) receiveBatch(messages); onMessage: (message) => {
}; receiveBatch([message]);
bridge[`restore${capitalized}History`] = receiveBatch; },
bridge[`prune${capitalized}HistoryView`] = () => { onBatch: receiveBatch,
prune(); restore: receiveBatch,
render(); prune: () => {
}; prune();
bridge[`reset${capitalized}HistoryView`] = reset; render();
bridge[`onServer${capitalized}`] = (message) => { },
receiveBatch([message]); reset
}; });
bridge.registerFt8FamilyBarRenderer?.(id, barFrames); bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => { const updatePeriod = () => {
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
@@ -91,7 +91,7 @@
}; };
bridge.updateFt8Bar = update; bridge.updateFt8Bar = update;
bridge.clearFt8Bar = () => { bridge.clearFt8Bar = () => {
({ ft8: bridge.resetFt8HistoryView, ft4: bridge.resetFt4HistoryView, ft2: bridge.resetFt2HistoryView })[active]?.(); bridge.trxPluginRuntime.reset(active);
}; };
bridge.closeFt8Bar = () => { bridge.closeFt8Bar = () => {
dismissed[active] = Date.now(); dismissed[active] = Date.now();
@@ -209,19 +209,19 @@
} }
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html }; return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
}; };
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; bridge.trxPluginRuntime.registerDecoder({
bridge[`onServer${capitalized}Batch`] = (messages) => { id,
if (Array.isArray(messages)) receiveBatch(messages); onMessage: (message) => {
}; receiveBatch([message]);
bridge[`restore${capitalized}History`] = receiveBatch; },
bridge[`prune${capitalized}HistoryView`] = () => { onBatch: receiveBatch,
prune(); restore: receiveBatch,
render(); prune: () => {
}; prune();
bridge[`reset${capitalized}HistoryView`] = reset; render();
bridge[`onServer${capitalized}`] = (message) => { },
receiveBatch([message]); reset
}; });
bridge.registerFt8FamilyBarRenderer?.(id, barFrames); bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => { const updatePeriod = () => {
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`; if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
@@ -207,29 +207,39 @@
}); });
} }
} }
satWindow.onServerLrptProgress = function(msg) { function onServerLrptProgress(msg) {
if (satDom.status && (msg.mcu_count ?? 0) > 0) { if (satDom.status && (msg.mcu_count ?? 0) > 0) {
satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`; satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
} }
}; }
satWindow.onServerLrptImage = function(msg) { function onServerLrptImage(msg) {
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)"; if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt"); addSatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) { if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
satWindow.addSatMapOverlay(msg); satWindow.addSatMapOverlay(msg);
} }
}; }
satWindow.resetSatHistoryView = function() { function resetSatHistoryView() {
satImageHistory = []; satImageHistory = [];
if (satDom.historyList) satDom.historyList.innerHTML = ""; if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard(); renderSatLatestCard();
renderSatHistoryTable(); renderSatHistoryTable();
satWindow.clearSatMapOverlays?.(); satWindow.clearSatMapOverlays?.();
}; }
satWindow.pruneSatHistoryView = function() { function pruneSatHistoryView() {
renderSatHistoryTable(); renderSatHistoryTable();
renderSatLatestCard(); renderSatLatestCard();
}; }
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_image",
onMessage: onServerLrptImage,
reset: resetSatHistoryView,
prune: pruneSatHistoryView
});
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_progress",
onMessage: onServerLrptProgress
});
var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn"); var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
lrptDecodeToggleBtn?.addEventListener("click", () => { lrptDecodeToggleBtn?.addEventListener("click", () => {
void (async () => { void (async () => {
@@ -257,7 +267,7 @@
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return; if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try { try {
await satWindow.postPath?.("/clear_lrpt_decode"); await satWindow.postPath?.("/clear_lrpt_decode");
satWindow.resetSatHistoryView?.(); resetSatHistoryView();
} catch (e) { } catch (e) {
console.error("Weather satellite history clear failed", e); console.error("Weather satellite history clear failed", e);
} }
@@ -205,7 +205,7 @@
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable); scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
} }
} }
wefaxWindow.onServerWefaxProgress = function(msg) { function onServerWefaxProgress(msg) {
if (msg.state && !msg.line_data) { if (msg.state && !msg.line_data) {
if (wefaxDom.status) { if (wefaxDom.status) {
wefaxDom.status.textContent = msg.state; wefaxDom.status.textContent = msg.state;
@@ -229,16 +229,16 @@
wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`; wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`;
wefaxDom.status.style.color = "var(--text-accent)"; wefaxDom.status.style.color = "var(--text-accent)";
} }
}; }
wefaxWindow.onServerWefax = function(msg) { function onServerWefax(msg) {
addWefaxImage(msg); addWefaxImage(msg);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none"; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
if (wefaxDom.status) { if (wefaxDom.status) {
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`; wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
wefaxDom.status.style.color = ""; wefaxDom.status.style.color = "";
} }
}; }
wefaxWindow.restoreWefaxHistory = function(messages) { function restoreWefaxHistory(messages) {
if (!messages.length) return; if (!messages.length) return;
for (const message of messages) { for (const message of messages) {
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now(); const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
@@ -255,13 +255,13 @@
if (wefaxActiveView === "history") { if (wefaxActiveView === "history") {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable); scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
} }
}; }
wefaxWindow.pruneWefaxHistoryView = function() { function pruneWefaxHistoryView() {
pruneWefaxHistory(); pruneWefaxHistory();
renderWefaxHistoryTable(); renderWefaxHistoryTable();
renderWefaxLatestCard(); renderWefaxLatestCard();
}; }
wefaxWindow.resetWefaxHistoryView = function() { function resetWefaxHistoryView() {
wefaxImageHistory = []; wefaxImageHistory = [];
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = ""; if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none"; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
@@ -273,7 +273,7 @@
wefaxDom.status.textContent = "Idle"; wefaxDom.status.textContent = "Idle";
wefaxDom.status.style.color = ""; wefaxDom.status.style.color = "";
} }
}; }
if (wefaxDom.filterInput) { if (wefaxDom.filterInput) {
const filterInput = wefaxDom.filterInput; const filterInput = wefaxDom.filterInput;
wefaxDom.filterInput.addEventListener("input", function() { wefaxDom.filterInput.addEventListener("input", function() {
@@ -313,7 +313,7 @@
void (async () => { void (async () => {
try { try {
await wefaxWindow.postPath?.("/clear_wefax_decode"); await wefaxWindow.postPath?.("/clear_wefax_decode");
wefaxWindow.resetWefaxHistoryView?.(); resetWefaxHistoryView();
} catch (e) { } catch (e) {
console.error("WEFAX clear failed", e); console.error("WEFAX clear failed", e);
} }
@@ -321,4 +321,15 @@
}); });
} }
renderWefaxLatestCard(); renderWefaxLatestCard();
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax",
onMessage: onServerWefax,
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
reset: resetWefaxHistoryView
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
onMessage: onServerWefaxProgress
});
})(); })();
@@ -97,7 +97,7 @@
} }
}; };
} }
wsprWindow.onServerWsprBatch = function(messages) { function onServerWsprBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return; if (!Array.isArray(messages) || messages.length === 0) return;
if (wsprStatus) wsprStatus.textContent = "Receiving"; if (wsprStatus) wsprStatus.textContent = "Receiving";
const normalized = []; const normalized = [];
@@ -116,15 +116,11 @@
wsprMessageHistory = normalized.concat(wsprMessageHistory); wsprMessageHistory = normalized.concat(wsprMessageHistory);
pruneWsprMessageHistory(); pruneWsprMessageHistory();
scheduleWsprHistoryRender(); scheduleWsprHistoryRender();
}; }
wsprWindow.restoreWsprHistory = function(messages) { function pruneWsprHistoryView() {
const callback = wsprWindow.onServerWsprBatch;
if (typeof callback === "function") callback(messages);
};
wsprWindow.pruneWsprHistoryView = function() {
pruneWsprMessageHistory(); pruneWsprMessageHistory();
renderWsprHistory(); renderWsprHistory();
}; }
function escapeWsprHtml(input) { function escapeWsprHtml(input) {
return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"); return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
} }
@@ -205,12 +201,12 @@
const message = row.dataset.message || ""; const message = row.dataset.message || "";
row.style.display = message.includes(wsprFilterText) ? "" : "none"; row.style.display = message.includes(wsprFilterText) ? "" : "none";
} }
wsprWindow.resetWsprHistoryView = function() { function resetWsprHistoryView() {
if (wsprMessagesEl) wsprMessagesEl.innerHTML = ""; if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
wsprMessageHistory = []; wsprMessageHistory = [];
renderWsprHistory(); renderWsprHistory();
if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr"); if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
}; }
if (wsprFilterInput) { if (wsprFilterInput) {
wsprFilterInput.addEventListener("input", () => { wsprFilterInput.addEventListener("input", () => {
wsprFilterText = wsprFilterInput.value.trim().toUpperCase(); wsprFilterText = wsprFilterInput.value.trim().toUpperCase();
@@ -246,13 +242,13 @@
if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return; if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try { try {
await wsprWindow.postPath?.("/clear_wspr_decode"); await wsprWindow.postPath?.("/clear_wspr_decode");
wsprWindow.resetWsprHistoryView?.(); resetWsprHistoryView();
} catch (error) { } catch (error) {
console.error("WSPR history clear failed", error); console.error("WSPR history clear failed", error);
} }
})(); })();
}); });
wsprWindow.onServerWspr = function(msg) { function onServerWspr(msg) {
if (wsprStatus) wsprStatus.textContent = "Receiving"; if (wsprStatus) wsprStatus.textContent = "Receiving";
const next = normalizeServerWsprMessage(msg); const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && wsprWindow.mapAddLocator) { if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
@@ -262,5 +258,13 @@
}); });
} }
addWsprMessage(next.history); addWsprMessage(next.history);
}; }
wsprWindow.trxPluginRuntime.registerDecoder({
id: "wspr",
onMessage: onServerWspr,
onBatch: onServerWsprBatch,
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
reset: resetWsprHistoryView
});
})(); })();
@@ -567,12 +567,9 @@ function currentDecodeHistoryRetentionMs() {
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs; window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
window.applyDecodeHistoryRetention = function() { window.applyDecodeHistoryRetention = function() {
window.trxPluginRuntime.prune("aprs"); for (const decoder of ["aprs", "hf_aprs", "ais", "vdes", "ft8", "ft4", "ft2", "wspr", "wefax"]) {
window.trxPluginRuntime.prune("hf_aprs"); window.trxPluginRuntime.prune(decoder);
window.trxPluginRuntime.prune("ais"); }
window.trxPluginRuntime.prune("vdes");
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
}; };
function syncTopBarAccess() { function syncTopBarAccess() {
@@ -6215,18 +6212,7 @@ function updateDecodeStatus(text) {
} }
} }
function dispatchDecodeMessage(msg, skipStats) { function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") { if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
window.trxPluginRuntime.dispatch(msg.type, msg);
}
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg);
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg);
if (msg.type === "lrpt_progress" && window.onServerLrptProgress) window.onServerLrptProgress(msg);
if (msg.type === "wefax" && window.onServerWefax) window.onServerWefax(msg);
if (msg.type === "wefax_progress" && window.onServerWefaxProgress) window.onServerWefaxProgress(msg);
if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") { if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") {
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
window.trx.modules.map?.scheduleStatsRender(); window.trx.modules.map?.scheduleStatsRender();
@@ -6244,27 +6230,9 @@ function dispatchDecodeBatch(batch) {
window.trx.modules.map?.scheduleStatsRender(); window.trx.modules.map?.scheduleStatsRender();
const type = String(batch[0]?.type || ""); const type = String(batch[0]?.type || "");
const uniformType = batch.every((msg) => String(msg?.type || "") === type); const uniformType = batch.every((msg) => String(msg?.type || "") === type);
if (uniformType) { if (uniformType && type) {
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") { window.trxPluginRuntime.dispatchBatch(type, batch);
window.trxPluginRuntime.dispatchBatch(type, batch); return;
return;
}
if (type === "ft8" && window.onServerFt8Batch) {
window.onServerFt8Batch(batch);
return;
}
if (type === "ft4" && window.onServerFt4Batch) {
window.onServerFt4Batch(batch);
return;
}
if (type === "ft2" && window.onServerFt2Batch) {
window.onServerFt2Batch(batch);
return;
}
if (type === "wspr" && window.onServerWsprBatch) {
window.onServerWsprBatch(batch);
return;
}
} }
for (const msg of batch) { for (const msg of batch) {
dispatchDecodeMessage(msg, true); dispatchDecodeMessage(msg, true);
@@ -6318,34 +6286,7 @@ function restoreDecodeHistoryGroup(kind, messages) {
} }
window.trx.modules.map?.scheduleStatsRender(); window.trx.modules.map?.scheduleStatsRender();
} }
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") { window.trxPluginRuntime.restore(kind, messages);
window.trxPluginRuntime.restore(kind, messages);
return;
}
if (kind === "cw" && window.restoreCwHistory) {
window.restoreCwHistory(messages);
return;
}
if (kind === "ft8" && window.restoreFt8History) {
window.restoreFt8History(messages);
return;
}
if (kind === "ft4" && window.restoreFt4History) {
window.restoreFt4History(messages);
return;
}
if (kind === "ft2" && window.restoreFt2History) {
window.restoreFt2History(messages);
return;
}
if (kind === "wspr" && window.restoreWsprHistory) {
window.restoreWsprHistory(messages);
return;
}
if (kind === "wefax" && window.restoreWefaxHistory) {
window.restoreWefaxHistory(messages);
return;
}
} }
function connectDecode() { function connectDecode() {
@@ -6354,14 +6295,7 @@ function connectDecode() {
decodeHistoryReplayActive = false; decodeHistoryReplayActive = false;
decodeMapSyncPending = false; decodeMapSyncPending = false;
window.trxPluginRuntime.clearQueued(); window.trxPluginRuntime.clearQueued();
window.trxPluginRuntime.reset("ais"); window.trxPluginRuntime.resetAll();
window.trxPluginRuntime.reset("vdes");
window.trxPluginRuntime.reset("aprs");
window.trxPluginRuntime.reset("hf_aprs");
if (window.resetCwHistoryView) window.resetCwHistoryView();
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
// Buffer live messages until history fetch settles so history always appears // Buffer live messages until history fetch settles so history always appears
// before any live updates, regardless of network ordering. // before any live updates, regardless of network ordering.
@@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {}; export {};
type Rgba = [number, number, number, number]; type Rgba = [number, number, number, number];
@@ -36,12 +38,9 @@ interface CwBridge {
updateCwBar?: () => void; updateCwBar?: () => void;
clearCwBar?: () => void; clearCwBar?: () => void;
closeCwBar?: () => void; closeCwBar?: () => void;
resetCwHistoryView?: () => void;
onServerCw?: (event: CwEvent) => void;
restoreCwHistory?: (events: CwEvent[]) => void;
refreshCwTonePicker?: () => void; refreshCwTonePicker?: () => void;
} }
const cwWindow = window as unknown as CwBridge; const cwWindow = window as unknown as CwBridge & PluginRuntimeWindow;
// --- CW (Morse) Decoder Plugin (server-side decode) --- // --- CW (Morse) Decoder Plugin (server-side decode) ---
const cwStatusEl = document.getElementById("cw-status"); const cwStatusEl = document.getElementById("cw-status");
@@ -154,7 +153,7 @@ function updateCwBar(): void {
} }
cwWindow.updateCwBar = updateCwBar; cwWindow.updateCwBar = updateCwBar;
cwWindow.clearCwBar = function() { cwWindow.clearCwBar = function() {
cwWindow.resetCwHistoryView?.(); resetCwHistoryView();
}; };
cwWindow.closeCwBar = function() { cwWindow.closeCwBar = function() {
cwBarDismissedAtMs = Date.now(); cwBarDismissedAtMs = Date.now();
@@ -401,21 +400,21 @@ if (cwToneCanvas) {
}); });
} }
cwWindow.resetCwHistoryView = function() { function resetCwHistoryView(): void {
if (cwOutputEl) cwOutputEl.innerHTML = ""; if (cwOutputEl) cwOutputEl.innerHTML = "";
cwLastAppendTime = 0; cwLastAppendTime = 0;
cwBarHistory = []; cwBarHistory = [];
cwBarCurrentLine = null; cwBarCurrentLine = null;
updateCwBar(); updateCwBar();
drawCwTonePicker(); drawCwTonePicker();
}; }
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => { document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
void (async () => { void (async () => {
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return; if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try { try {
await cwWindow.postPath?.("/clear_cw_decode"); await cwWindow.postPath?.("/clear_cw_decode");
cwWindow.resetCwHistoryView?.(); resetCwHistoryView();
} catch (error: unknown) { } catch (error: unknown) {
console.error("CW history clear failed", error); console.error("CW history clear failed", error);
} }
@@ -423,7 +422,7 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
}); });
// --- Server-side CW decode handler --- // --- Server-side CW decode handler ---
cwWindow.onServerCw = function(evt: CwEvent) { function onServerCw(evt: CwEvent): void {
if (cwStatusEl) cwStatusEl.textContent = "Receiving"; if (cwStatusEl) cwStatusEl.textContent = "Receiving";
if (evt.text && cwOutputEl) { if (evt.text && cwOutputEl) {
// Append decoded text to output // Append decoded text to output
@@ -479,15 +478,22 @@ cwWindow.onServerCw = function(evt: CwEvent) {
cwTonePickerRaf = null; cwTonePickerRaf = null;
drawCwTonePicker(); drawCwTonePicker();
}); });
}; }
cwWindow.restoreCwHistory = function(events: CwEvent[]) { function restoreCwHistory(events: CwEvent[]): void {
if (!Array.isArray(events) || events.length === 0) return; if (!Array.isArray(events) || events.length === 0) return;
if (cwStatusEl) cwStatusEl.textContent = "Receiving"; if (cwStatusEl) cwStatusEl.textContent = "Receiving";
for (const evt of events) { for (const evt of events) {
cwWindow.onServerCw?.(evt); onServerCw(evt);
} }
}; }
cwWindow.trxPluginRuntime.registerDecoder({
id: "cw",
onMessage: onServerCw,
restore: restoreCwHistory,
reset: resetCwHistoryView,
});
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() { cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
ensureCwToneCanvasResolution(); ensureCwToneCanvasResolution();
@@ -2,9 +2,11 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export type FtxDecoderId = "ft2" | "ft4" | "ft8"; export type FtxDecoderId = "ft2" | "ft4" | "ft8";
interface FtxMessage { export interface FtxMessage {
message?: string | undefined; message?: string | undefined;
ts_ms?: number | undefined; ts_ms?: number | undefined;
_tsMs?: number | undefined; _tsMs?: number | undefined;
@@ -46,9 +48,6 @@ interface FtxBridge {
postPath?: (path: string) => Promise<unknown>; postPath?: (path: string) => Promise<unknown>;
fmtTime?: (timestampMs: number) => string; fmtTime?: (timestampMs: number) => string;
trxUi: ConfirmApi; trxUi: ConfirmApi;
resetFt2HistoryView?: () => void;
resetFt4HistoryView?: () => void;
resetFt8HistoryView?: () => void;
clearFt8Bar?: () => void; clearFt8Bar?: () => void;
closeFt8Bar?: () => void; closeFt8Bar?: () => void;
[key: string]: unknown; [key: string]: unknown;
@@ -61,7 +60,7 @@ interface FtxConfig {
periodDigits?: number; periodDigits?: number;
} }
const bridge = window as unknown as FtxBridge; const bridge = window as unknown as FtxBridge & PluginRuntimeWindow;
function finiteNumber(value: unknown): number | null { function finiteNumber(value: unknown): number | null {
const number = typeof value === "number" ? value : Number(value); const number = typeof value === "number" ? value : Number(value);
@@ -164,7 +163,7 @@ export function initializeFt8FamilyBar(): void {
bridge.setFt8FamilyBarDecoder = (decoder) => { active = decoder; update(); }; bridge.setFt8FamilyBarDecoder = (decoder) => { active = decoder; update(); };
bridge.updateFt8Bar = update; bridge.updateFt8Bar = update;
bridge.clearFt8Bar = () => { bridge.clearFt8Bar = () => {
({ ft8: bridge.resetFt8HistoryView, ft4: bridge.resetFt4HistoryView, ft2: bridge.resetFt2HistoryView })[active]?.(); bridge.trxPluginRuntime.reset(active);
}; };
bridge.closeFt8Bar = () => { bridge.closeFt8Bar = () => {
dismissed[active] = Date.now(); dismissed[active] = Date.now();
@@ -285,12 +284,14 @@ export function initializeFtxDecoder(config: FtxConfig): void {
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html }; return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
}; };
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; bridge.trxPluginRuntime.registerDecoder({
bridge[`onServer${capitalized}Batch`] = (messages: FtxMessage[]) => { if (Array.isArray(messages)) receiveBatch(messages); }; id,
bridge[`restore${capitalized}History`] = receiveBatch; onMessage: (message: FtxMessage) => { receiveBatch([message]); },
bridge[`prune${capitalized}HistoryView`] = () => { prune(); render(); }; onBatch: receiveBatch,
bridge[`reset${capitalized}HistoryView`] = reset; restore: receiveBatch,
bridge[`onServer${capitalized}`] = (message: FtxMessage) => { receiveBatch([message]); }; prune: () => { prune(); render(); },
reset,
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames); bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = (): void => { const updatePeriod = (): void => {
@@ -3,17 +3,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import type { LrptProgress, SatelliteImage, SatelliteLiveUpdate, SatellitePass, SatellitePassResponse } from "./satellite-types"; import type { LrptProgress, SatelliteImage, SatelliteLiveUpdate, SatellitePass, SatellitePassResponse } from "./satellite-types";
import type { PluginRuntimeWindow } from "./runtime-contract.js";
type SatelliteView = "live" | "history" | "predictions"; type SatelliteView = "live" | "history" | "predictions";
interface SatelliteBridge { interface SatelliteBridge {
trxScheduleUiFrameJob?: (key: string, job: () => void) => void; trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
clearSatPredictionDom?: () => void; clearSatPredictionDom?: () => void;
updateSatLiveState?: (update: SatelliteLiveUpdate) => void; updateSatLiveState?: (update: SatelliteLiveUpdate) => void;
onServerLrptProgress?: (message: LrptProgress) => void;
onServerLrptImage?: (message: SatelliteImage) => void;
addSatMapOverlay?: (image: SatelliteImage) => void; addSatMapOverlay?: (image: SatelliteImage) => void;
resetSatHistoryView?: () => void;
pruneSatHistoryView?: () => void;
clearSatMapOverlays?: () => void; clearSatMapOverlays?: () => void;
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>; takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
postPath?: (path: string) => Promise<unknown>; postPath?: (path: string) => Promise<unknown>;
@@ -22,7 +19,7 @@ interface SatelliteBridge {
enableMapSourceFilter?: (source: string) => void; enableMapSourceFilter?: (source: string) => void;
navigateToAprsMap?: (lat: number, lon: number) => void; navigateToAprsMap?: (lat: number, lon: number) => void;
} }
const satWindow = window as unknown as SatelliteBridge; const satWindow = window as unknown as SatelliteBridge & PluginRuntimeWindow;
// --- SAT Plugin --- // --- SAT Plugin ---
// Live view: decoder state, latest image card // Live view: decoder state, latest image card
@@ -265,32 +262,43 @@ function addSatImage(img: SatelliteImage, decoder: "lrpt"): void {
} }
// ── Server callbacks ──────────────────────────────────────────────── // ── Server callbacks ────────────────────────────────────────────────
satWindow.onServerLrptProgress = function (msg: LrptProgress) { function onServerLrptProgress(msg: LrptProgress): void {
if (satDom.status && (msg.mcu_count ?? 0) > 0) { if (satDom.status && (msg.mcu_count ?? 0) > 0) {
satDom.status.textContent = `Receiving \u2014 ${String(msg.mcu_count ?? 0)} MCU rows decoded`; satDom.status.textContent = `Receiving \u2014 ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
} }
}; }
satWindow.onServerLrptImage = function (msg: SatelliteImage) { function onServerLrptImage(msg: SatelliteImage): void {
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)"; if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt"); addSatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) { if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
satWindow.addSatMapOverlay(msg); satWindow.addSatMapOverlay(msg);
} }
}; }
satWindow.resetSatHistoryView = function () { function resetSatHistoryView(): void {
satImageHistory = []; satImageHistory = [];
if (satDom.historyList) satDom.historyList.innerHTML = ""; if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard(); renderSatLatestCard();
renderSatHistoryTable(); renderSatHistoryTable();
satWindow.clearSatMapOverlays?.(); satWindow.clearSatMapOverlays?.();
}; }
satWindow.pruneSatHistoryView = function () { function pruneSatHistoryView(): void {
renderSatHistoryTable(); renderSatHistoryTable();
renderSatLatestCard(); renderSatLatestCard();
}; }
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_image",
onMessage: onServerLrptImage,
reset: resetSatHistoryView,
prune: pruneSatHistoryView,
});
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_progress",
onMessage: onServerLrptProgress,
});
// ── Toggle buttons ────────────────────────────────────────────────── // ── Toggle buttons ──────────────────────────────────────────────────
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn"); const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
@@ -320,7 +328,7 @@ document
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return; if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try { try {
await satWindow.postPath?.("/clear_lrpt_decode"); await satWindow.postPath?.("/clear_lrpt_decode");
satWindow.resetSatHistoryView?.(); resetSatHistoryView();
} catch (e) { } catch (e) {
console.error("Weather satellite history clear failed", e); console.error("Weather satellite history clear failed", e);
} }
@@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {}; export {};
interface WefaxImage { interface WefaxImage {
@@ -29,15 +31,10 @@ interface WefaxBridge {
trxScheduleUiFrameJob?: (key: string, job: () => void) => void; trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
postPath?: (path: string) => Promise<unknown>; postPath?: (path: string) => Promise<unknown>;
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>; takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
onServerWefaxProgress?: (message: WefaxProgress) => void;
onServerWefax?: (message: WefaxImage) => void;
restoreWefaxHistory?: (messages: WefaxImage[]) => void;
pruneWefaxHistoryView?: () => void;
resetWefaxHistoryView?: () => void;
syncWefaxToggle?: (enabled: boolean) => void; syncWefaxToggle?: (enabled: boolean) => void;
} }
const wefaxWindow = window as unknown as WefaxBridge; const wefaxWindow = window as unknown as WefaxBridge & PluginRuntimeWindow;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// wefax.js — WEFAX decoder plugin for trx-frontend-http // wefax.js — WEFAX decoder plugin for trx-frontend-http
@@ -299,7 +296,7 @@ function addWefaxImage(msg: WefaxImage): void {
} }
// ── SSE event handlers (public API) ───────────────────────────────── // ── SSE event handlers (public API) ─────────────────────────────────
wefaxWindow.onServerWefaxProgress = function (msg: WefaxProgress) { function onServerWefaxProgress(msg: WefaxProgress): void {
// State-only update (no image data): show decoder state in status. // State-only update (no image data): show decoder state in status.
if (msg.state && !msg.line_data) { if (msg.state && !msg.line_data) {
if (wefaxDom.status) { if (wefaxDom.status) {
@@ -329,9 +326,9 @@ wefaxWindow.onServerWefaxProgress = function (msg: WefaxProgress) {
wefaxDom.status.textContent = `Receiving \u2014 line ${String(msg.line_count ?? 0)}`; wefaxDom.status.textContent = `Receiving \u2014 line ${String(msg.line_count ?? 0)}`;
wefaxDom.status.style.color = 'var(--text-accent)'; wefaxDom.status.style.color = 'var(--text-accent)';
} }
}; }
wefaxWindow.onServerWefax = function (msg: WefaxImage) { function onServerWefax(msg: WefaxImage): void {
addWefaxImage(msg); addWefaxImage(msg);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none'; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
@@ -339,9 +336,9 @@ wefaxWindow.onServerWefax = function (msg: WefaxImage) {
wefaxDom.status.textContent = `Complete \u2014 ${String(msg.line_count ?? 0)} lines`; wefaxDom.status.textContent = `Complete \u2014 ${String(msg.line_count ?? 0)} lines`;
wefaxDom.status.style.color = ''; wefaxDom.status.style.color = '';
} }
}; }
wefaxWindow.restoreWefaxHistory = function (messages: WefaxImage[]) { function restoreWefaxHistory(messages: WefaxImage[]): void {
if (!messages.length) return; if (!messages.length) return;
for (const message of messages) { for (const message of messages) {
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now(); const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
@@ -358,15 +355,15 @@ wefaxWindow.restoreWefaxHistory = function (messages: WefaxImage[]) {
if (wefaxActiveView === 'history') { if (wefaxActiveView === 'history') {
scheduleWefaxUi('wefax-history', renderWefaxHistoryTable); scheduleWefaxUi('wefax-history', renderWefaxHistoryTable);
} }
}; }
wefaxWindow.pruneWefaxHistoryView = function () { function pruneWefaxHistoryView(): void {
pruneWefaxHistory(); pruneWefaxHistory();
renderWefaxHistoryTable(); renderWefaxHistoryTable();
renderWefaxLatestCard(); renderWefaxLatestCard();
}; }
wefaxWindow.resetWefaxHistoryView = function () { function resetWefaxHistoryView(): void {
wefaxImageHistory = []; wefaxImageHistory = [];
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = ''; if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = '';
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none'; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
@@ -378,7 +375,7 @@ wefaxWindow.resetWefaxHistoryView = function () {
wefaxDom.status.textContent = 'Idle'; wefaxDom.status.textContent = 'Idle';
wefaxDom.status.style.color = ''; wefaxDom.status.style.color = '';
} }
}; }
// ── Filter / sort handlers ────────────────────────────────────────── // ── Filter / sort handlers ──────────────────────────────────────────
if (wefaxDom.filterInput) { if (wefaxDom.filterInput) {
@@ -424,7 +421,7 @@ if (wefaxDom.clearBtn) {
wefaxDom.clearBtn.addEventListener('click', () => { void (async () => { wefaxDom.clearBtn.addEventListener('click', () => { void (async () => {
try { try {
await wefaxWindow.postPath?.('/clear_wefax_decode'); await wefaxWindow.postPath?.('/clear_wefax_decode');
wefaxWindow.resetWefaxHistoryView?.(); resetWefaxHistoryView();
} catch (e) { } catch (e) {
console.error('WEFAX clear failed', e); console.error('WEFAX clear failed', e);
} }
@@ -433,3 +430,15 @@ if (wefaxDom.clearBtn) {
// ── Initial render ────────────────────────────────────────────────── // ── Initial render ──────────────────────────────────────────────────
renderWefaxLatestCard(); renderWefaxLatestCard();
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax",
onMessage: onServerWefax,
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
reset: resetWefaxHistoryView,
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
onMessage: onServerWefaxProgress,
});
@@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {}; export {};
interface WsprMessage { interface WsprMessage {
@@ -25,14 +27,9 @@ interface WsprBridge {
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<void>; takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<void>;
postPath?: (path: string) => Promise<unknown>; postPath?: (path: string) => Promise<unknown>;
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> }; trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
onServerWsprBatch?: (messages: WsprMessage[]) => void;
restoreWsprHistory?: (messages: WsprMessage[]) => void;
pruneWsprHistoryView?: () => void;
resetWsprHistoryView?: () => void;
onServerWspr?: (message: WsprMessage) => void;
[key: string]: unknown; [key: string]: unknown;
} }
const wsprWindow = window as unknown as WsprBridge; const wsprWindow = window as unknown as WsprBridge & PluginRuntimeWindow;
// --- WSPR Decoder Plugin (server-side decode) --- // --- WSPR Decoder Plugin (server-side decode) ---
const wsprStatus = document.getElementById("wspr-status"); const wsprStatus = document.getElementById("wspr-status");
@@ -142,7 +139,7 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
}; };
} }
wsprWindow.onServerWsprBatch = function(messages: WsprMessage[]) { function onServerWsprBatch(messages: WsprMessage[]): void {
if (!Array.isArray(messages) || messages.length === 0) return; if (!Array.isArray(messages) || messages.length === 0) return;
if (wsprStatus) wsprStatus.textContent = "Receiving"; if (wsprStatus) wsprStatus.textContent = "Receiving";
const normalized: WsprMessage[] = []; const normalized: WsprMessage[] = [];
@@ -161,17 +158,12 @@ wsprWindow.onServerWsprBatch = function(messages: WsprMessage[]) {
wsprMessageHistory = normalized.concat(wsprMessageHistory); wsprMessageHistory = normalized.concat(wsprMessageHistory);
pruneWsprMessageHistory(); pruneWsprMessageHistory();
scheduleWsprHistoryRender(); scheduleWsprHistoryRender();
}; }
wsprWindow.restoreWsprHistory = function(messages: WsprMessage[]) { function pruneWsprHistoryView(): void {
const callback = wsprWindow.onServerWsprBatch;
if (typeof callback === "function") callback(messages);
};
wsprWindow.pruneWsprHistoryView = function() {
pruneWsprMessageHistory(); pruneWsprMessageHistory();
renderWsprHistory(); renderWsprHistory();
}; }
function escapeWsprHtml(input: string): string { function escapeWsprHtml(input: string): string {
return input return input
@@ -266,12 +258,12 @@ function applyWsprFilterToRow(row: HTMLElement): void {
row.style.display = message.includes(wsprFilterText) ? "" : "none"; row.style.display = message.includes(wsprFilterText) ? "" : "none";
} }
wsprWindow.resetWsprHistoryView = function() { function resetWsprHistoryView(): void {
if (wsprMessagesEl) wsprMessagesEl.innerHTML = ""; if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
wsprMessageHistory = []; wsprMessageHistory = [];
renderWsprHistory(); renderWsprHistory();
if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr"); if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
}; }
if (wsprFilterInput) { if (wsprFilterInput) {
wsprFilterInput.addEventListener("input", () => { wsprFilterInput.addEventListener("input", () => {
@@ -311,14 +303,14 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return; if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try { try {
await wsprWindow.postPath?.("/clear_wspr_decode"); await wsprWindow.postPath?.("/clear_wspr_decode");
wsprWindow.resetWsprHistoryView?.(); resetWsprHistoryView();
} catch (error: unknown) { } catch (error: unknown) {
console.error("WSPR history clear failed", error); console.error("WSPR history clear failed", error);
} }
})(); })();
}); });
wsprWindow.onServerWspr = function(msg: WsprMessage) { function onServerWspr(msg: WsprMessage): void {
if (wsprStatus) wsprStatus.textContent = "Receiving"; if (wsprStatus) wsprStatus.textContent = "Receiving";
const next = normalizeServerWsprMessage(msg); const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && wsprWindow.mapAddLocator) { if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
@@ -328,4 +320,13 @@ wsprWindow.onServerWspr = function(msg: WsprMessage) {
}); });
} }
addWsprMessage(next.history); addWsprMessage(next.history);
}; }
wsprWindow.trxPluginRuntime.registerDecoder({
id: "wspr",
onMessage: onServerWspr,
onBatch: onServerWsprBatch,
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
reset: resetWsprHistoryView,
});
@@ -37,13 +37,15 @@ test("CW entry appends server-decoded text and registers lifecycle callbacks", a
Math, Math,
console, 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/cw.js", import.meta.url), "utf8"); const source = await readFile(new URL("../../assets/web/generated/cw.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context); new vm.Script(source).runInContext(context);
window.onServerCw({ text: "CQ", wpm: 18, tone_hz: 700, signal_on: true }); window.trxPluginRuntime.dispatch("cw", { text: "CQ", wpm: 18, tone_hz: 700, signal_on: true });
assert.equal(status.textContent, "Receiving"); assert.equal(status.textContent, "Receiving");
assert.equal(output.children.length, 1); assert.equal(output.children.length, 1);
assert.equal(output.children[0].textContent, "CQ"); assert.equal(output.children[0].textContent, "CQ");
assert.equal(typeof window.resetCwHistoryView, "function"); assert.equal(window.onServerCw, undefined);
assert.equal(typeof window.restoreCwHistory, "function"); assert.equal(window.trxPluginRuntime.hasDecoder("cw"), true);
}); });
@@ -29,10 +29,12 @@ test("FT2 entry normalizes audio offsets and registers typed callbacks", async (
Array, Array,
console, 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 readFile(new URL("../../assets/web/generated/ft2.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context); new vm.Script(source).runInContext(context);
window.onServerFt2({ message: "CQ SP0ABC JO91", freq_hz: 1_250, ts_ms: Date.now(), snr_db: -8 }); window.trxPluginRuntime.dispatch("ft2", { message: "CQ SP0ABC JO91", freq_hz: 1_250, ts_ms: Date.now(), snr_db: -8 });
assert.equal(mapMessages.length, 1); assert.equal(mapMessages.length, 1);
assert.equal(mapMessages[0][4].freq_hz, 14_075_250); assert.equal(mapMessages[0][4].freq_hz, 14_075_250);
const bar = barRenderer(); const bar = barRenderer();
@@ -59,12 +61,14 @@ test("FT8 entry installs shared parsing without relying on script globals", asyn
Set, Set,
console, 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 readFile(new URL("../../assets/web/generated/ft8.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context); new vm.Script(source).runInContext(context);
const details = window.ft8ExtractLocatorDetails("K1ABC SP0ABC JO91"); const details = window.ft8ExtractLocatorDetails("K1ABC SP0ABC JO91");
assert.equal(details[0].source, "SP0ABC"); assert.equal(details[0].source, "SP0ABC");
assert.equal(details[0].target, "K1ABC"); assert.equal(details[0].target, "K1ABC");
window.onServerFt8({ message: "K1ABC SP0ABC JO91", freq_hz: 500, ts_ms: Date.now() }); window.trxPluginRuntime.dispatch("ft8", { message: "K1ABC SP0ABC JO91", freq_hz: 500, ts_ms: Date.now() });
assert.equal(forwarded[0][4].freq_hz, 7_074_500); assert.equal(forwarded[0][4].freq_hz, 7_074_500);
}); });
@@ -26,12 +26,14 @@ test("satellite entry registers lifecycle callbacks and forwards georeferenced i
Error, Error,
console, 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/sat.js", import.meta.url), "utf8"); const source = await readFile(new URL("../../assets/web/generated/sat.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context); new vm.Script(source).runInContext(context);
window.onServerLrptImage({ path: "/images/lrpt.png", geo_bounds: [50, 10, 55, 20], mcu_count: 100 }); window.trxPluginRuntime.dispatch("lrpt_image", { path: "/images/lrpt.png", geo_bounds: [50, 10, 55, 20], mcu_count: 100 });
assert.equal(overlays.length, 1); assert.equal(overlays.length, 1);
assert.deepEqual(overlays[0].geo_bounds, [50, 10, 55, 20]); assert.deepEqual(overlays[0].geo_bounds, [50, 10, 55, 20]);
assert.equal(typeof window.updateSatLiveState, "function"); assert.equal(typeof window.updateSatLiveState, "function");
assert.equal(typeof window.resetSatHistoryView, "function"); assert.equal(window.onServerLrptImage, undefined);
}); });
@@ -19,12 +19,14 @@ test("WEFAX entry exposes typed lifecycle handlers and renders decoder state", a
Uint8Array, Uint8Array,
console, 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/wefax.js", import.meta.url), "utf8"); const source = await readFile(new URL("../../assets/web/generated/wefax.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context); new vm.Script(source).runInContext(context);
assert.equal(typeof window.onServerWefaxProgress, "function"); assert.equal(window.onServerWefaxProgress, undefined);
assert.equal(typeof window.restoreWefaxHistory, "function"); assert.equal(window.trxPluginRuntime.hasDecoder("wefax_progress"), true);
window.onServerWefaxProgress({ state: "Scanning 12 MHz" }); window.trxPluginRuntime.dispatch("wefax_progress", { state: "Scanning 12 MHz" });
assert.equal(status.textContent, "Scanning 12 MHz"); assert.equal(status.textContent, "Scanning 12 MHz");
assert.equal(status.style.color, "var(--text-accent)"); assert.equal(status.style.color, "var(--text-accent)");
}); });
@@ -25,10 +25,12 @@ test("WSPR entry forwards decoded locators at their absolute frequency", async (
Element: class Element {}, Element: class Element {},
console, 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/wspr.js", import.meta.url), "utf8"); const source = await readFile(new URL("../../assets/web/generated/wspr.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context); new vm.Script(source).runInContext(context);
window.onServerWspr({ message: "SP0ABC JO91 37", freq_hz: 1_420, ts_ms: Date.now() }); window.trxPluginRuntime.dispatch("wspr", { message: "SP0ABC JO91 37", freq_hz: 1_420, ts_ms: Date.now() });
assert.equal(forwarded.length, 1); assert.equal(forwarded.length, 1);
assert.equal(forwarded[0][1][0], "JO91"); assert.equal(forwarded[0][1][0], "JO91");
assert.equal(forwarded[0][4].freq_hz, 14_097_020); assert.equal(forwarded[0][4].freq_hz, 14_097_020);