Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf3bcc8a84 |
@@ -454,6 +454,29 @@ listener and every rig's audio listener.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Multiple Rigs in the Web UI
|
||||||
|
|
||||||
|
A client connected to several rigs decodes all of them at once, whichever one
|
||||||
|
is on screen. The rig picker in the header decides which rig the UI is about,
|
||||||
|
and each page answers that differently:
|
||||||
|
|
||||||
|
| Page | Shows |
|
||||||
|
|------|-------|
|
||||||
|
| Radio | The selected rig: its spectrum, its audio, and the mini decode views over the waterfall. |
|
||||||
|
| Digital Modes | The selected rig: every decoder panel, its counts and its status line. |
|
||||||
|
| Map | The whole station — every rig's positions, with the map's own rig filter to narrow it. |
|
||||||
|
| Statistics | The whole station, including the per-rig comparison. |
|
||||||
|
|
||||||
|
Switching rigs repaints the radio and digital modes pages for the rig now
|
||||||
|
selected. Nothing is lost by switching: the traffic other rigs heard is still
|
||||||
|
held, and switching back brings it up again. The one exception is the CW pane,
|
||||||
|
which is a single running stream of copied text rather than a list of frames,
|
||||||
|
so it starts empty on the rig you switch to.
|
||||||
|
|
||||||
|
Each browser tab keeps its own selection, so two tabs can watch two rigs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tune Links
|
## Tune Links
|
||||||
|
|
||||||
Every page of the web UI carries what the radio is doing in its address, so the
|
Every page of the web UI carries what the radio is doing in its address, so the
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
forActiveRig,
|
||||||
isActiveRigDecode
|
isActiveRigDecode
|
||||||
} from "./chunk-BQQXSNLC.js";
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore,
|
hostCore,
|
||||||
hostState
|
hostState
|
||||||
@@ -143,18 +144,22 @@ function aisLatestByVessel(messages) {
|
|||||||
}
|
}
|
||||||
return Array.from(byMmsi.values());
|
return Array.from(byMmsi.values());
|
||||||
}
|
}
|
||||||
|
function aisRigMessages() {
|
||||||
|
return forActiveRig(aisMessageHistory);
|
||||||
|
}
|
||||||
function updateAisSummary() {
|
function updateAisSummary() {
|
||||||
const plan = currentAisChannelPlan();
|
const plan = currentAisChannelPlan();
|
||||||
if (aisChannelSummaryEl) {
|
if (aisChannelSummaryEl) {
|
||||||
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
|
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
|
||||||
}
|
}
|
||||||
const vessels = aisLatestByVessel(aisMessageHistory);
|
const rigMessages = aisRigMessages();
|
||||||
|
const vessels = aisLatestByVessel(rigMessages);
|
||||||
if (aisVesselCountEl) {
|
if (aisVesselCountEl) {
|
||||||
const count = vessels.length;
|
const count = vessels.length;
|
||||||
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
|
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
|
||||||
}
|
}
|
||||||
if (aisLatestSeenEl) {
|
if (aisLatestSeenEl) {
|
||||||
const latest = aisMessageHistory[0];
|
const latest = rigMessages[0];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
aisLatestSeenEl.textContent = "No traffic yet";
|
aisLatestSeenEl.textContent = "No traffic yet";
|
||||||
} else {
|
} else {
|
||||||
@@ -272,7 +277,7 @@ function renderAisHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const message of aisMessageHistory) {
|
for (const message of aisRigMessages()) {
|
||||||
fragment.appendChild(renderAisRow(message));
|
fragment.appendChild(renderAisRow(message));
|
||||||
}
|
}
|
||||||
aisMessagesEl.replaceChildren(fragment);
|
aisMessagesEl.replaceChildren(fragment);
|
||||||
@@ -304,10 +309,10 @@ function normalizeServerAisMessage(msg) {
|
|||||||
}
|
}
|
||||||
function onServerAisBatch(messages) {
|
function onServerAisBatch(messages) {
|
||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerAisMessage(msg);
|
const next = normalizeServerAisMessage(msg);
|
||||||
|
if (aisStatus && isActiveRigDecode(next.rig_id)) aisStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
@@ -347,8 +352,9 @@ if (aisFilterInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onServerAis(msg) {
|
function onServerAis(msg) {
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
const message = normalizeServerAisMessage(msg);
|
||||||
addAisMessage(normalizeServerAisMessage(msg));
|
if (aisStatus && isActiveRigDecode(message.rig_id)) aisStatus.textContent = "Receiving";
|
||||||
|
addAisMessage(message);
|
||||||
}
|
}
|
||||||
updateAisSummary();
|
updateAisSummary();
|
||||||
window.trxPluginRuntime.registerDecoder({
|
window.trxPluginRuntime.registerDecoder({
|
||||||
@@ -358,6 +364,10 @@ window.trxPluginRuntime.registerDecoder({
|
|||||||
restore: onServerAisBatch,
|
restore: onServerAisBatch,
|
||||||
reset: resetAisHistoryView,
|
reset: resetAisHistoryView,
|
||||||
prune: pruneAisHistoryView,
|
prune: pruneAisHistoryView,
|
||||||
|
rerender: () => {
|
||||||
|
updateAisBar();
|
||||||
|
renderAisHistory();
|
||||||
|
},
|
||||||
// Oldest first, so vessel tracks are rebuilt in the order they happened.
|
// Oldest first, so vessel tracks are rebuilt in the order they happened.
|
||||||
syncMap: () => {
|
syncMap: () => {
|
||||||
for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry);
|
for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry);
|
||||||
|
|||||||
@@ -1065,6 +1065,9 @@ var runtime = {
|
|||||||
plugin.prune();
|
plugin.prune();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
rerenderAll() {
|
||||||
|
for (const plugin of decoders.values()) plugin.rerender?.();
|
||||||
|
},
|
||||||
syncMapAll() {
|
syncMapAll() {
|
||||||
for (const plugin of decoders.values()) plugin.syncMap?.();
|
for (const plugin of decoders.values()) plugin.syncMap?.();
|
||||||
},
|
},
|
||||||
@@ -3577,6 +3580,7 @@ function resetDecoderStateOnRigSwitch() {
|
|||||||
if (el) el.textContent = "--";
|
if (el) el.textContent = "--";
|
||||||
});
|
});
|
||||||
refreshDecodeBars();
|
refreshDecodeBars();
|
||||||
|
window.trxPluginRuntime.rerenderAll();
|
||||||
}
|
}
|
||||||
function resetWfmStereoIndicator() {
|
function resetWfmStereoIndicator() {
|
||||||
if (!wfmStFlagEl) return;
|
if (!wfmStFlagEl) return;
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
renderAprsPacketRow
|
renderAprsPacketRow
|
||||||
} from "./chunk-OPEIVJGD.js";
|
} from "./chunk-OPEIVJGD.js";
|
||||||
import {
|
import {
|
||||||
|
forActiveRig,
|
||||||
isActiveRigDecode
|
isActiveRigDecode
|
||||||
} from "./chunk-BQQXSNLC.js";
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore,
|
hostCore,
|
||||||
hostState
|
hostState
|
||||||
@@ -85,20 +86,25 @@ function aprsFilterMatch(pkt) {
|
|||||||
].filter(Boolean).join(" ").toUpperCase();
|
].filter(Boolean).join(" ").toUpperCase();
|
||||||
return haystack.includes(aprsFilterText);
|
return haystack.includes(aprsFilterText);
|
||||||
}
|
}
|
||||||
|
function aprsRigPackets() {
|
||||||
|
return forActiveRig(aprsPacketHistory);
|
||||||
|
}
|
||||||
function aprsVisiblePackets() {
|
function aprsVisiblePackets() {
|
||||||
const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
|
const rigPackets = aprsRigPackets();
|
||||||
|
const packets = aprsCollapseDup ? collapseAprsDuplicates(rigPackets) : rigPackets;
|
||||||
return packets.filter(aprsFilterMatch);
|
return packets.filter(aprsFilterMatch);
|
||||||
}
|
}
|
||||||
function updateAprsSummary() {
|
function updateAprsSummary() {
|
||||||
|
const rigPackets = aprsRigPackets();
|
||||||
const visible = aprsVisiblePackets();
|
const visible = aprsVisiblePackets();
|
||||||
if (aprsTotalCountEl) {
|
if (aprsTotalCountEl) {
|
||||||
aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
|
aprsTotalCountEl.textContent = `${rigPackets.length} total`;
|
||||||
}
|
}
|
||||||
if (aprsVisibleCountEl) {
|
if (aprsVisibleCountEl) {
|
||||||
aprsVisibleCountEl.textContent = `${visible.length} shown`;
|
aprsVisibleCountEl.textContent = `${visible.length} shown`;
|
||||||
}
|
}
|
||||||
if (aprsLatestSeenEl) {
|
if (aprsLatestSeenEl) {
|
||||||
const latest = aprsPacketHistory[0];
|
const latest = rigPackets[0];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
aprsLatestSeenEl.textContent = "No packets yet";
|
aprsLatestSeenEl.textContent = "No packets yet";
|
||||||
} else {
|
} else {
|
||||||
@@ -220,11 +226,11 @@ function normalizeServerAprsPacket(pkt) {
|
|||||||
}
|
}
|
||||||
function onServerAprsBatch(packets) {
|
function onServerAprsBatch(packets) {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
let hasCrcOk = false;
|
let hasCrcOk = false;
|
||||||
for (const pkt of packets) {
|
for (const pkt of packets) {
|
||||||
const next = normalizeServerAprsPacket(pkt);
|
const next = normalizeServerAprsPacket(pkt);
|
||||||
|
if (aprsStatus && isActiveRigDecode(next.rig_id)) aprsStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
@@ -282,8 +288,9 @@ if (aprsFilterInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onServerAprs(pkt) {
|
function onServerAprs(pkt) {
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
const packet = normalizeServerAprsPacket(pkt);
|
||||||
addAprsPacket(normalizeServerAprsPacket(pkt));
|
if (aprsStatus && isActiveRigDecode(packet.rig_id)) aprsStatus.textContent = "Receiving";
|
||||||
|
addAprsPacket(packet);
|
||||||
}
|
}
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
window.trxPluginRuntime.registerDecoder({
|
window.trxPluginRuntime.registerDecoder({
|
||||||
@@ -293,6 +300,10 @@ window.trxPluginRuntime.registerDecoder({
|
|||||||
restore: onServerAprsBatch,
|
restore: onServerAprsBatch,
|
||||||
reset: resetAprsHistoryView,
|
reset: resetAprsHistoryView,
|
||||||
prune: pruneAprsHistoryView,
|
prune: pruneAprsHistoryView,
|
||||||
|
rerender: () => {
|
||||||
|
updateAprsBar();
|
||||||
|
renderAprsHistory();
|
||||||
|
},
|
||||||
// Oldest first, so station tracks are rebuilt in the order they happened.
|
// Oldest first, so station tracks are rebuilt in the order they happened.
|
||||||
syncMap: () => {
|
syncMap: () => {
|
||||||
for (const entry of [...aprsPacketHistory].reverse()) plotAprsPacket(entry);
|
for (const entry of [...aprsPacketHistory].reverse()) plotAprsPacket(entry);
|
||||||
|
|||||||
+12
-4
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
forActiveRig,
|
||||||
isActiveRigDecode
|
isActiveRigDecode
|
||||||
} from "./chunk-BQQXSNLC.js";
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore
|
hostCore
|
||||||
} from "./chunk-KL66PICH.js";
|
} from "./chunk-KL66PICH.js";
|
||||||
@@ -156,9 +157,10 @@ function initializeFtxDecoder(config) {
|
|||||||
const render = () => {
|
const render = () => {
|
||||||
prune();
|
prune();
|
||||||
if (!messagesElement) return;
|
if (!messagesElement) return;
|
||||||
|
const rigMessages = forActiveRig(history);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const message of history) {
|
for (const message of rigMessages) {
|
||||||
if (count >= 200) break;
|
if (count >= 200) break;
|
||||||
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
||||||
fragment.appendChild(renderRow(message));
|
fragment.appendChild(renderRow(message));
|
||||||
@@ -194,7 +196,9 @@ function initializeFtxDecoder(config) {
|
|||||||
};
|
};
|
||||||
const receiveBatch = (messages) => {
|
const receiveBatch = (messages) => {
|
||||||
if (messages.length === 0) return;
|
if (messages.length === 0) return;
|
||||||
if (status) status.textContent = "Receiving";
|
if (status && messages.some((message) => isActiveRigDecode(message.rig_id))) {
|
||||||
|
status.textContent = "Receiving";
|
||||||
|
}
|
||||||
history = messages.map(normalize).reverse().concat(history);
|
history = messages.map(normalize).reverse().concat(history);
|
||||||
prune();
|
prune();
|
||||||
bridge.setFt8FamilyBarDecoder?.(id);
|
bridge.setFt8FamilyBarDecoder?.(id);
|
||||||
@@ -232,7 +236,11 @@ function initializeFtxDecoder(config) {
|
|||||||
prune();
|
prune();
|
||||||
render();
|
render();
|
||||||
},
|
},
|
||||||
reset
|
reset,
|
||||||
|
rerender: () => {
|
||||||
|
bridge.updateFt8Bar?.();
|
||||||
|
render();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||||
const updatePeriod = () => {
|
const updatePeriod = () => {
|
||||||
+5
-1
@@ -8,7 +8,11 @@ function isActiveRigDecode(rigId) {
|
|||||||
if (!activeRigId || !rigId) return true;
|
if (!activeRigId || !rigId) return true;
|
||||||
return rigId === activeRigId;
|
return rigId === activeRigId;
|
||||||
}
|
}
|
||||||
|
function forActiveRig(items) {
|
||||||
|
return items.filter((item) => isActiveRigDecode(item.rig_id));
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isActiveRigDecode
|
isActiveRigDecode,
|
||||||
|
forActiveRig
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
isActiveRigDecode
|
isActiveRigDecode
|
||||||
} from "./chunk-BQQXSNLC.js";
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore
|
hostCore
|
||||||
} from "./chunk-KL66PICH.js";
|
} from "./chunk-KL66PICH.js";
|
||||||
@@ -329,8 +329,9 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
function onServerCw(evt) {
|
function onServerCw(evt) {
|
||||||
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
|
const forSelectedRig = isActiveRigDecode(evt.rig_id ?? null);
|
||||||
if (evt.text && cwOutputEl) {
|
if (cwStatusEl && forSelectedRig) cwStatusEl.textContent = "Receiving";
|
||||||
|
if (evt.text && cwOutputEl && forSelectedRig) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 1e4 || evt.text === "\n") {
|
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 1e4 || evt.text === "\n") {
|
||||||
const line = document.createElement("div");
|
const line = document.createElement("div");
|
||||||
@@ -370,10 +371,10 @@ function onServerCw(evt) {
|
|||||||
}
|
}
|
||||||
updateCwBar();
|
updateCwBar();
|
||||||
}
|
}
|
||||||
if (cwSignalIndicator) {
|
if (cwSignalIndicator && forSelectedRig) {
|
||||||
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
|
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
|
||||||
}
|
}
|
||||||
if (!cwAutoInput || cwAutoInput.checked) {
|
if (forSelectedRig && (!cwAutoInput || cwAutoInput.checked)) {
|
||||||
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
|
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
|
||||||
cwWpmInput.value = String(clampCwWpm(evt.wpm));
|
cwWpmInput.value = String(clampCwWpm(evt.wpm));
|
||||||
}
|
}
|
||||||
@@ -398,7 +399,14 @@ cwWindow.trxPluginRuntime.registerDecoder({
|
|||||||
id: "cw",
|
id: "cw",
|
||||||
onMessage: onServerCw,
|
onMessage: onServerCw,
|
||||||
restore: restoreCwHistory,
|
restore: restoreCwHistory,
|
||||||
reset: resetCwHistoryView
|
reset: resetCwHistoryView,
|
||||||
|
// The copied text of a rig that is no longer on screen cannot be unpicked
|
||||||
|
// from the pane, so the switch starts the new rig's stream from empty.
|
||||||
|
rerender: () => {
|
||||||
|
if (cwOutputEl) cwOutputEl.innerHTML = "";
|
||||||
|
cwLastAppendTime = 0;
|
||||||
|
updateCwBar();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
||||||
ensureCwToneCanvasResolution();
|
ensureCwToneCanvasResolution();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
initializeFtxDecoder
|
initializeFtxDecoder
|
||||||
} from "./chunk-K3D6FOP5.js";
|
} from "./chunk-PAKFJPA2.js";
|
||||||
import "./chunk-BQQXSNLC.js";
|
import "./chunk-S57W63QN.js";
|
||||||
import "./chunk-KL66PICH.js";
|
import "./chunk-KL66PICH.js";
|
||||||
|
|
||||||
// src/plugins/ft2.ts
|
// src/plugins/ft2.ts
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
initializeFtxDecoder
|
initializeFtxDecoder
|
||||||
} from "./chunk-K3D6FOP5.js";
|
} from "./chunk-PAKFJPA2.js";
|
||||||
import "./chunk-BQQXSNLC.js";
|
import "./chunk-S57W63QN.js";
|
||||||
import "./chunk-KL66PICH.js";
|
import "./chunk-KL66PICH.js";
|
||||||
|
|
||||||
// src/plugins/ft4.ts
|
// src/plugins/ft4.ts
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {
|
|||||||
initializeFt8FamilyBar,
|
initializeFt8FamilyBar,
|
||||||
initializeFtxDecoder,
|
initializeFtxDecoder,
|
||||||
installFtxCompatibilityHelpers
|
installFtxCompatibilityHelpers
|
||||||
} from "./chunk-K3D6FOP5.js";
|
} from "./chunk-PAKFJPA2.js";
|
||||||
import "./chunk-BQQXSNLC.js";
|
import "./chunk-S57W63QN.js";
|
||||||
import "./chunk-KL66PICH.js";
|
import "./chunk-KL66PICH.js";
|
||||||
|
|
||||||
// src/plugins/ft8.ts
|
// src/plugins/ft8.ts
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {
|
|||||||
normalizeAprsPacket,
|
normalizeAprsPacket,
|
||||||
renderAprsPacketRow
|
renderAprsPacketRow
|
||||||
} from "./chunk-OPEIVJGD.js";
|
} from "./chunk-OPEIVJGD.js";
|
||||||
|
import {
|
||||||
|
forActiveRig,
|
||||||
|
isActiveRigDecode
|
||||||
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore,
|
hostCore,
|
||||||
hostState
|
hostState
|
||||||
@@ -67,21 +71,26 @@ function hfAprsFilterMatch(pkt) {
|
|||||||
].filter(Boolean).join(" ").toUpperCase();
|
].filter(Boolean).join(" ").toUpperCase();
|
||||||
return haystack.includes(hfAprsFilterText);
|
return haystack.includes(hfAprsFilterText);
|
||||||
}
|
}
|
||||||
|
function hfAprsRigPackets() {
|
||||||
|
return forActiveRig(hfAprsPacketHistory);
|
||||||
|
}
|
||||||
function hfAprsVisiblePackets() {
|
function hfAprsVisiblePackets() {
|
||||||
const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
|
const rigPackets = hfAprsRigPackets();
|
||||||
|
const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(rigPackets) : rigPackets;
|
||||||
return packets.filter(hfAprsFilterMatch);
|
return packets.filter(hfAprsFilterMatch);
|
||||||
}
|
}
|
||||||
var collapseHfAprsDuplicates = collapseAprsDuplicates;
|
var collapseHfAprsDuplicates = collapseAprsDuplicates;
|
||||||
function updateHfAprsSummary() {
|
function updateHfAprsSummary() {
|
||||||
|
const rigPackets = hfAprsRigPackets();
|
||||||
const visible = hfAprsVisiblePackets();
|
const visible = hfAprsVisiblePackets();
|
||||||
if (hfAprsTotalCountEl) {
|
if (hfAprsTotalCountEl) {
|
||||||
hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
|
hfAprsTotalCountEl.textContent = `${rigPackets.length} total`;
|
||||||
}
|
}
|
||||||
if (hfAprsVisibleCountEl) {
|
if (hfAprsVisibleCountEl) {
|
||||||
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
|
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
|
||||||
}
|
}
|
||||||
if (hfAprsLatestSeenEl) {
|
if (hfAprsLatestSeenEl) {
|
||||||
const latest = hfAprsPacketHistory[0];
|
const latest = rigPackets[0];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
hfAprsLatestSeenEl.textContent = "No packets yet";
|
hfAprsLatestSeenEl.textContent = "No packets yet";
|
||||||
} else {
|
} else {
|
||||||
@@ -158,10 +167,10 @@ function normalizeServerHfAprsPacket(pkt) {
|
|||||||
}
|
}
|
||||||
function onServerHfAprsBatch(packets) {
|
function onServerHfAprsBatch(packets) {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
for (const pkt of packets) {
|
for (const pkt of packets) {
|
||||||
const next = normalizeServerHfAprsPacket(pkt);
|
const next = normalizeServerHfAprsPacket(pkt);
|
||||||
|
if (hfAprsStatus && isActiveRigDecode(next.rig_id)) hfAprsStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
@@ -227,8 +236,9 @@ if (hfAprsFilterInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onServerHfAprs(pkt) {
|
function onServerHfAprs(pkt) {
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
const packet = normalizeServerHfAprsPacket(pkt);
|
||||||
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
if (hfAprsStatus && isActiveRigDecode(packet.rig_id)) hfAprsStatus.textContent = "Receiving";
|
||||||
|
addHfAprsPacket(packet);
|
||||||
}
|
}
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
window.trxPluginRuntime.registerDecoder({
|
window.trxPluginRuntime.registerDecoder({
|
||||||
@@ -237,5 +247,6 @@ window.trxPluginRuntime.registerDecoder({
|
|||||||
onBatch: onServerHfAprsBatch,
|
onBatch: onServerHfAprsBatch,
|
||||||
restore: onServerHfAprsBatch,
|
restore: onServerHfAprsBatch,
|
||||||
reset: resetHfAprsHistoryView,
|
reset: resetHfAprsHistoryView,
|
||||||
prune: pruneHfAprsHistoryView
|
prune: pruneHfAprsHistoryView,
|
||||||
|
rerender: renderHfAprsHistory
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
forActiveRig,
|
||||||
|
isActiveRigDecode
|
||||||
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore
|
hostCore
|
||||||
} from "./chunk-KL66PICH.js";
|
} from "./chunk-KL66PICH.js";
|
||||||
@@ -111,6 +115,7 @@ function paintRow(line, rgb) {
|
|||||||
liveRows = Math.max(liveRows, line + 1);
|
liveRows = Math.max(liveRows, line + 1);
|
||||||
}
|
}
|
||||||
function onProgress(msg) {
|
function onProgress(msg) {
|
||||||
|
if (!isActiveRigDecode(msg.rig_id)) return;
|
||||||
if (msg.state) {
|
if (msg.state) {
|
||||||
if (sstvDom.status) sstvDom.status.textContent = msg.state;
|
if (sstvDom.status) sstvDom.status.textContent = msg.state;
|
||||||
beginPicture(msg.mode || "", msg.width || 0, msg.height || 0);
|
beginPicture(msg.mode || "", msg.width || 0, msg.height || 0);
|
||||||
@@ -131,15 +136,20 @@ function onImage(msg) {
|
|||||||
sstvHistory.push(image);
|
sstvHistory.push(image);
|
||||||
if (sstvHistory.length > SSTV_MAX_IMAGES) sstvHistory.shift();
|
if (sstvHistory.length > SSTV_MAX_IMAGES) sstvHistory.shift();
|
||||||
pruneHistory();
|
pruneHistory();
|
||||||
|
if (!isActiveRigDecode(image.rig_id)) return;
|
||||||
if (sstvDom.status) {
|
if (sstvDom.status) {
|
||||||
sstvDom.status.textContent = image.complete ? `Received ${image.mode ?? "picture"}` : `Partial ${image.mode ?? "picture"} — ${image.lines ?? 0} lines`;
|
sstvDom.status.textContent = image.complete ? `Received ${image.mode ?? "picture"}` : `Partial ${image.mode ?? "picture"} — ${image.lines ?? 0} lines`;
|
||||||
}
|
}
|
||||||
scheduleUi("sstv-latest", renderLatestCard);
|
scheduleUi("sstv-latest", renderLatestCard);
|
||||||
if (activeView === "history") scheduleUi("sstv-history", renderHistoryTable);
|
if (activeView === "history") scheduleUi("sstv-history", renderHistoryTable);
|
||||||
}
|
}
|
||||||
|
function sstvRigImages() {
|
||||||
|
return forActiveRig(sstvHistory);
|
||||||
|
}
|
||||||
function renderLatestCard() {
|
function renderLatestCard() {
|
||||||
if (!sstvDom.liveLatest) return;
|
if (!sstvDom.liveLatest) return;
|
||||||
const latest = sstvHistory[sstvHistory.length - 1];
|
const rigImages = sstvRigImages();
|
||||||
|
const latest = rigImages[rigImages.length - 1];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
sstvDom.liveLatest.innerHTML = "";
|
sstvDom.liveLatest.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
@@ -160,8 +170,9 @@ function renderLatestCard() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
function filteredHistory() {
|
function filteredHistory() {
|
||||||
|
const rigImages = sstvRigImages();
|
||||||
const text = filterText.trim().toLowerCase();
|
const text = filterText.trim().toLowerCase();
|
||||||
const matching = text ? sstvHistory.filter((image) => (image.mode ?? "").toLowerCase().includes(text)) : sstvHistory.slice();
|
const matching = text ? rigImages.filter((image) => (image.mode ?? "").toLowerCase().includes(text)) : rigImages;
|
||||||
const newestFirst = (sstvDom.sortSelect?.value ?? "newest") === "newest";
|
const newestFirst = (sstvDom.sortSelect?.value ?? "newest") === "newest";
|
||||||
matching.sort((a, b) => (newestFirst ? 1 : -1) * ((b._tsMs ?? 0) - (a._tsMs ?? 0)));
|
matching.sort((a, b) => (newestFirst ? 1 : -1) * ((b._tsMs ?? 0) - (a._tsMs ?? 0)));
|
||||||
return matching;
|
return matching;
|
||||||
@@ -241,7 +252,11 @@ sstvWindow.trxPluginRuntime.registerDecoder({
|
|||||||
onMessage: onImage,
|
onMessage: onImage,
|
||||||
restore: restoreHistory,
|
restore: restoreHistory,
|
||||||
prune: renderHistoryTable,
|
prune: renderHistoryTable,
|
||||||
reset: resetHistoryView
|
reset: resetHistoryView,
|
||||||
|
rerender: () => {
|
||||||
|
renderLatestCard();
|
||||||
|
renderHistoryTable();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
sstvWindow.trxPluginRuntime.registerDecoder({
|
sstvWindow.trxPluginRuntime.registerDecoder({
|
||||||
id: "sstv_progress",
|
id: "sstv_progress",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
forActiveRig,
|
||||||
isActiveRigDecode
|
isActiveRigDecode
|
||||||
} from "./chunk-BQQXSNLC.js";
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore
|
hostCore
|
||||||
} from "./chunk-KL66PICH.js";
|
} from "./chunk-KL66PICH.js";
|
||||||
@@ -63,17 +64,21 @@ function vdesHexPreview(rawBytes) {
|
|||||||
if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
|
if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
|
||||||
return rawBytes.slice(0, 20).map((value) => value.toString(16).padStart(2, "0")).join(" ").toUpperCase();
|
return rawBytes.slice(0, 20).map((value) => value.toString(16).padStart(2, "0")).join(" ").toUpperCase();
|
||||||
}
|
}
|
||||||
|
function vdesRigMessages() {
|
||||||
|
return forActiveRig(vdesMessageHistory);
|
||||||
|
}
|
||||||
function updateVdesSummary() {
|
function updateVdesSummary() {
|
||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
if (vdesChannelSummaryEl) {
|
if (vdesChannelSummaryEl) {
|
||||||
vdesChannelSummaryEl.textContent = currentVdesCenterText();
|
vdesChannelSummaryEl.textContent = currentVdesCenterText();
|
||||||
}
|
}
|
||||||
|
const rigMessages = vdesRigMessages();
|
||||||
if (vdesFrameCountEl) {
|
if (vdesFrameCountEl) {
|
||||||
const count = vdesMessageHistory.length;
|
const count = rigMessages.length;
|
||||||
vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
|
vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
|
||||||
}
|
}
|
||||||
if (vdesLatestSeenEl) {
|
if (vdesLatestSeenEl) {
|
||||||
const latest = vdesMessageHistory[0];
|
const latest = rigMessages[0];
|
||||||
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
|
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +190,7 @@ function renderVdesHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const message of vdesMessageHistory) {
|
for (const message of vdesRigMessages()) {
|
||||||
fragment.appendChild(renderVdesRow(message));
|
fragment.appendChild(renderVdesRow(message));
|
||||||
}
|
}
|
||||||
vdesMessagesEl.replaceChildren(fragment);
|
vdesMessagesEl.replaceChildren(fragment);
|
||||||
@@ -212,10 +217,10 @@ function normalizeServerVdesMessage(msg) {
|
|||||||
}
|
}
|
||||||
function onServerVdesBatch(messages) {
|
function onServerVdesBatch(messages) {
|
||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerVdesMessage(msg);
|
const next = normalizeServerVdesMessage(msg);
|
||||||
|
if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
@@ -254,8 +259,8 @@ function plotVdesMessage(msg) {
|
|||||||
vdesWindow.vdesMapAddPoint(msg);
|
vdesWindow.vdesMapAddPoint(msg);
|
||||||
}
|
}
|
||||||
function onServerVdes(msg) {
|
function onServerVdes(msg) {
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
|
||||||
const next = normalizeServerVdesMessage(msg);
|
const next = normalizeServerVdesMessage(msg);
|
||||||
|
if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
|
||||||
addVdesMessage(next);
|
addVdesMessage(next);
|
||||||
plotVdesMessage(next);
|
plotVdesMessage(next);
|
||||||
}
|
}
|
||||||
@@ -272,6 +277,10 @@ window.trxPluginRuntime.registerDecoder({
|
|||||||
restore: onServerVdesBatch,
|
restore: onServerVdesBatch,
|
||||||
reset: resetVdesHistoryView,
|
reset: resetVdesHistoryView,
|
||||||
prune: pruneVdesHistoryView,
|
prune: pruneVdesHistoryView,
|
||||||
|
rerender: () => {
|
||||||
|
updateVdesBar();
|
||||||
|
renderVdesHistory();
|
||||||
|
},
|
||||||
// Oldest first, so tracks are rebuilt in the order they happened.
|
// Oldest first, so tracks are rebuilt in the order they happened.
|
||||||
syncMap: () => {
|
syncMap: () => {
|
||||||
for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry);
|
for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
forActiveRig,
|
||||||
|
isActiveRigDecode
|
||||||
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore
|
hostCore
|
||||||
} from "./chunk-KL66PICH.js";
|
} from "./chunk-KL66PICH.js";
|
||||||
@@ -102,13 +106,17 @@ function paintLine(lineBytes) {
|
|||||||
wefaxLiveCtx.putImageData(imgData, 0, y);
|
wefaxLiveCtx.putImageData(imgData, 0, y);
|
||||||
wefaxLiveLineCount++;
|
wefaxLiveLineCount++;
|
||||||
}
|
}
|
||||||
|
function wefaxRigImages() {
|
||||||
|
return forActiveRig(wefaxImageHistory);
|
||||||
|
}
|
||||||
function renderWefaxLatestCard() {
|
function renderWefaxLatestCard() {
|
||||||
if (!wefaxDom.liveLatest) return;
|
if (!wefaxDom.liveLatest) return;
|
||||||
if (wefaxImageHistory.length === 0) {
|
const rigImages = wefaxRigImages();
|
||||||
|
if (rigImages.length === 0) {
|
||||||
wefaxDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
wefaxDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const img = wefaxImageHistory[0];
|
const img = rigImages[0];
|
||||||
if (!img) return;
|
if (!img) return;
|
||||||
const ts = img._ts || "--";
|
const ts = img._ts || "--";
|
||||||
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
||||||
@@ -129,7 +137,7 @@ function renderWefaxLatestCard() {
|
|||||||
wefaxDom.liveLatest.innerHTML = html;
|
wefaxDom.liveLatest.innerHTML = html;
|
||||||
}
|
}
|
||||||
function getWefaxFilteredHistory() {
|
function getWefaxFilteredHistory() {
|
||||||
let items = wefaxImageHistory;
|
let items = wefaxRigImages();
|
||||||
if (wefaxFilterText) {
|
if (wefaxFilterText) {
|
||||||
items = items.filter(function(i) {
|
items = items.filter(function(i) {
|
||||||
const haystack = [
|
const haystack = [
|
||||||
@@ -173,7 +181,7 @@ function renderWefaxHistoryTable() {
|
|||||||
}
|
}
|
||||||
wefaxDom.historyList.replaceChildren(fragment);
|
wefaxDom.historyList.replaceChildren(fragment);
|
||||||
if (wefaxDom.historyCount) {
|
if (wefaxDom.historyCount) {
|
||||||
const total = wefaxImageHistory.length;
|
const total = wefaxRigImages().length;
|
||||||
const shown = items.length;
|
const shown = items.length;
|
||||||
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${String(total)} image${total === 1 ? "" : "s"}` : `${String(shown)} of ${String(total)} images`;
|
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${String(total)} image${total === 1 ? "" : "s"}` : `${String(shown)} of ${String(total)} images`;
|
||||||
}
|
}
|
||||||
@@ -208,6 +216,7 @@ function addWefaxImage(msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onServerWefaxProgress(msg) {
|
function onServerWefaxProgress(msg) {
|
||||||
|
if (!isActiveRigDecode(msg.rig_id)) return;
|
||||||
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;
|
||||||
@@ -234,6 +243,7 @@ function onServerWefaxProgress(msg) {
|
|||||||
}
|
}
|
||||||
function onServerWefax(msg) {
|
function onServerWefax(msg) {
|
||||||
addWefaxImage(msg);
|
addWefaxImage(msg);
|
||||||
|
if (!isActiveRigDecode(msg.rig_id)) return;
|
||||||
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`;
|
||||||
@@ -328,7 +338,11 @@ wefaxWindow.trxPluginRuntime.registerDecoder({
|
|||||||
onMessage: onServerWefax,
|
onMessage: onServerWefax,
|
||||||
restore: restoreWefaxHistory,
|
restore: restoreWefaxHistory,
|
||||||
prune: pruneWefaxHistoryView,
|
prune: pruneWefaxHistoryView,
|
||||||
reset: resetWefaxHistoryView
|
reset: resetWefaxHistoryView,
|
||||||
|
rerender: () => {
|
||||||
|
renderWefaxLatestCard();
|
||||||
|
renderWefaxHistoryTable();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
wefaxWindow.trxPluginRuntime.registerDecoder({
|
wefaxWindow.trxPluginRuntime.registerDecoder({
|
||||||
id: "wefax_progress",
|
id: "wefax_progress",
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
forActiveRig,
|
||||||
|
isActiveRigDecode
|
||||||
|
} from "./chunk-S57W63QN.js";
|
||||||
import {
|
import {
|
||||||
hostCore
|
hostCore
|
||||||
} from "./chunk-KL66PICH.js";
|
} from "./chunk-KL66PICH.js";
|
||||||
@@ -64,10 +68,10 @@ function renderWsprRow(msg) {
|
|||||||
function renderWsprHistory() {
|
function renderWsprHistory() {
|
||||||
pruneWsprMessageHistory();
|
pruneWsprMessageHistory();
|
||||||
if (!wsprMessagesEl) return;
|
if (!wsprMessagesEl) return;
|
||||||
|
const rigMessages = forActiveRig(wsprMessageHistory);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (let i = 0; i < wsprMessageHistory.length; i += 1) {
|
for (const message of rigMessages) {
|
||||||
const message = wsprMessageHistory[i];
|
fragment.appendChild(renderWsprRow(message));
|
||||||
if (message) fragment.appendChild(renderWsprRow(message));
|
|
||||||
}
|
}
|
||||||
wsprMessagesEl.replaceChildren(fragment);
|
wsprMessagesEl.replaceChildren(fragment);
|
||||||
}
|
}
|
||||||
@@ -90,6 +94,7 @@ function normalizeServerWsprMessage(msg) {
|
|||||||
station,
|
station,
|
||||||
rfHz,
|
rfHz,
|
||||||
history: {
|
history: {
|
||||||
|
rig_id: msg.rig_id ?? null,
|
||||||
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
||||||
ts_ms: msg.ts_ms,
|
ts_ms: msg.ts_ms,
|
||||||
snr_db: msg.snr_db,
|
snr_db: msg.snr_db,
|
||||||
@@ -101,10 +106,10 @@ function normalizeServerWsprMessage(msg) {
|
|||||||
}
|
}
|
||||||
function onServerWsprBatch(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";
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerWsprMessage(msg);
|
const next = normalizeServerWsprMessage(msg);
|
||||||
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
||||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
||||||
...msg,
|
...msg,
|
||||||
@@ -251,7 +256,7 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
function onServerWspr(msg) {
|
function onServerWspr(msg) {
|
||||||
if (wsprStatus) wsprStatus.textContent = "Receiving";
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) 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) {
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
||||||
@@ -267,5 +272,6 @@ wsprWindow.trxPluginRuntime.registerDecoder({
|
|||||||
onBatch: onServerWsprBatch,
|
onBatch: onServerWsprBatch,
|
||||||
restore: onServerWsprBatch,
|
restore: onServerWsprBatch,
|
||||||
prune: pruneWsprHistoryView,
|
prune: pruneWsprHistoryView,
|
||||||
reset: resetWsprHistoryView
|
reset: resetWsprHistoryView,
|
||||||
|
rerender: renderWsprHistory
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2240,9 +2240,10 @@ function resetDecoderStateOnRigSwitch() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The decode stream is not rig-scoped, so the histories behind the mini views
|
// The decode stream is not rig-scoped, so the histories behind the mini views
|
||||||
// survive the switch: drop the outgoing rig's frames from them now instead of
|
// and the decoder panels survive the switch: repaint both for the rig now
|
||||||
// leaving them on screen until the next state update.
|
// selected instead of leaving the outgoing rig's traffic on screen.
|
||||||
refreshDecodeBars();
|
refreshDecodeBars();
|
||||||
|
window.trxPluginRuntime.rerenderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWfmStereoIndicator() {
|
function resetWfmStereoIndicator() {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const runtime: TrxPluginRuntime = {
|
|||||||
plugin.prune();
|
plugin.prune();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
rerenderAll() { for (const plugin of decoders.values()) plugin.rerender?.(); },
|
||||||
syncMapAll() { for (const plugin of decoders.values()) plugin.syncMap?.(); },
|
syncMapAll() { for (const plugin of decoders.values()) plugin.syncMap?.(); },
|
||||||
clearQueued() { queued.clear(); },
|
clearQueued() { queued.clear(); },
|
||||||
hasDecoder: (id) => decoders.has(id),
|
hasDecoder: (id) => decoders.has(id),
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
import { hostState } from "./host.js";
|
import { hostState } from "./host.js";
|
||||||
|
|
||||||
// The decode panels are aggregate views on purpose: they list what every rig
|
// The decode stream is not rig-scoped: every rig a client is connected to sends
|
||||||
// heard, and the history they restore from is not rig-scoped either. The mini
|
// its decodes to every browser, each frame naming the rig that heard it (the
|
||||||
// views over the waterfall are not aggregates — they read as a caption for the
|
// client stamps `rig_id` as it leaves the audio connection). The map wants all
|
||||||
// spectrum underneath, so a frame a background rig copied on another band
|
// of them — it has its own filter, and plotting only one rig would empty it —
|
||||||
// belongs to a different picture entirely. Each decode names the rig that
|
// but the radio page and the decoder panels describe one rig at a time: the one
|
||||||
// heard it (the client stamps `rig_id` as the frame leaves the audio
|
// the operator selected, whose spectrum is on screen and whose audio is
|
||||||
// connection), which is all the overlays need to keep to the rig on screen.
|
// playing. A frame a background rig copied on another band belongs to neither
|
||||||
|
// picture, so both filter on this.
|
||||||
export function isActiveRigDecode(rigId: string | null | undefined): boolean {
|
export function isActiveRigDecode(rigId: string | null | undefined): boolean {
|
||||||
const activeRigId = hostState.lastActiveRigId;
|
const activeRigId = hostState.lastActiveRigId;
|
||||||
// Before the rig list has arrived, and for a decode that reached the browser
|
// Before the rig list has arrived, and for a decode that reached the browser
|
||||||
@@ -18,3 +19,8 @@ export function isActiveRigDecode(rigId: string | null | undefined): boolean {
|
|||||||
if (!activeRigId || !rigId) return true;
|
if (!activeRigId || !rigId) return true;
|
||||||
return rigId === activeRigId;
|
return rigId === activeRigId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The decodes of `items` that the selected rig heard, in their original order. */
|
||||||
|
export function forActiveRig<T extends { rig_id?: string | null | undefined }>(items: T[]): T[] {
|
||||||
|
return items.filter((item) => isActiveRigDecode(item.rig_id));
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { hostCore, hostState } from "./host.js";
|
|||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
import { isActiveRigDecode } from "./active-rig.js";
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import type { PluginRuntimeWindow } from "./runtime-contract";
|
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||||
|
|
||||||
interface AisMessage {
|
interface AisMessage {
|
||||||
@@ -191,20 +191,27 @@ function aisLatestByVessel(messages: AisMessage[]): AisMessage[] {
|
|||||||
return Array.from(byMmsi.values());
|
return Array.from(byMmsi.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What the selected rig heard. The history holds every rig's traffic, for
|
||||||
|
* the map; the panel is about the rig the operator is working. */
|
||||||
|
function aisRigMessages(): AisMessage[] {
|
||||||
|
return forActiveRig(aisMessageHistory);
|
||||||
|
}
|
||||||
|
|
||||||
function updateAisSummary() {
|
function updateAisSummary() {
|
||||||
const plan = currentAisChannelPlan();
|
const plan = currentAisChannelPlan();
|
||||||
if (aisChannelSummaryEl) {
|
if (aisChannelSummaryEl) {
|
||||||
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
|
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vessels = aisLatestByVessel(aisMessageHistory);
|
const rigMessages = aisRigMessages();
|
||||||
|
const vessels = aisLatestByVessel(rigMessages);
|
||||||
if (aisVesselCountEl) {
|
if (aisVesselCountEl) {
|
||||||
const count = vessels.length;
|
const count = vessels.length;
|
||||||
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
|
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aisLatestSeenEl) {
|
if (aisLatestSeenEl) {
|
||||||
const latest = aisMessageHistory[0];
|
const latest = rigMessages[0];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
aisLatestSeenEl.textContent = "No traffic yet";
|
aisLatestSeenEl.textContent = "No traffic yet";
|
||||||
} else {
|
} else {
|
||||||
@@ -371,7 +378,7 @@ function renderAisHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const message of aisMessageHistory) {
|
for (const message of aisRigMessages()) {
|
||||||
fragment.appendChild(renderAisRow(message));
|
fragment.appendChild(renderAisRow(message));
|
||||||
}
|
}
|
||||||
aisMessagesEl.replaceChildren(fragment);
|
aisMessagesEl.replaceChildren(fragment);
|
||||||
@@ -410,10 +417,11 @@ function normalizeServerAisMessage(msg: AisMessage): AisMessage {
|
|||||||
|
|
||||||
function onServerAisBatch(messages: AisMessage[]): void {
|
function onServerAisBatch(messages: AisMessage[]): void {
|
||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
|
||||||
const normalized: AisMessage[] = [];
|
const normalized: AisMessage[] = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerAisMessage(msg);
|
const next = normalizeServerAisMessage(msg);
|
||||||
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
|
if (aisStatus && isActiveRigDecode(next.rig_id)) aisStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
@@ -455,8 +463,9 @@ if (aisFilterInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onServerAis(msg: AisMessage): void {
|
function onServerAis(msg: AisMessage): void {
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
const message = normalizeServerAisMessage(msg);
|
||||||
addAisMessage(normalizeServerAisMessage(msg));
|
if (aisStatus && isActiveRigDecode(message.rig_id)) aisStatus.textContent = "Receiving";
|
||||||
|
addAisMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAisSummary();
|
updateAisSummary();
|
||||||
@@ -467,6 +476,7 @@ updateAisSummary();
|
|||||||
restore: onServerAisBatch,
|
restore: onServerAisBatch,
|
||||||
reset: resetAisHistoryView,
|
reset: resetAisHistoryView,
|
||||||
prune: pruneAisHistoryView,
|
prune: pruneAisHistoryView,
|
||||||
|
rerender: () => { updateAisBar(); renderAisHistory(); },
|
||||||
// Oldest first, so vessel tracks are rebuilt in the order they happened.
|
// Oldest first, so vessel tracks are rebuilt in the order they happened.
|
||||||
syncMap: () => { for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry); },
|
syncMap: () => { for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry); },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { hostCore, hostState } from "./host.js";
|
import { hostCore, hostState } from "./host.js";
|
||||||
|
|
||||||
import { isActiveRigDecode } from "./active-rig.js";
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import {
|
import {
|
||||||
aprsAgeText,
|
aprsAgeText,
|
||||||
aprsPacketCategory,
|
aprsPacketCategory,
|
||||||
@@ -107,21 +107,29 @@ function aprsFilterMatch(pkt: AprsPacket): boolean {
|
|||||||
return haystack.includes(aprsFilterText);
|
return haystack.includes(aprsFilterText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What the selected rig heard. The history holds every rig's traffic, for
|
||||||
|
* the map; the panel is about the rig the operator is working. */
|
||||||
|
function aprsRigPackets(): AprsPacket[] {
|
||||||
|
return forActiveRig(aprsPacketHistory);
|
||||||
|
}
|
||||||
|
|
||||||
function aprsVisiblePackets(): AprsPacket[] {
|
function aprsVisiblePackets(): AprsPacket[] {
|
||||||
const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
|
const rigPackets = aprsRigPackets();
|
||||||
|
const packets = aprsCollapseDup ? collapseAprsDuplicates(rigPackets) : rigPackets;
|
||||||
return packets.filter(aprsFilterMatch);
|
return packets.filter(aprsFilterMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAprsSummary() {
|
function updateAprsSummary() {
|
||||||
|
const rigPackets = aprsRigPackets();
|
||||||
const visible = aprsVisiblePackets();
|
const visible = aprsVisiblePackets();
|
||||||
if (aprsTotalCountEl) {
|
if (aprsTotalCountEl) {
|
||||||
aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
|
aprsTotalCountEl.textContent = `${rigPackets.length} total`;
|
||||||
}
|
}
|
||||||
if (aprsVisibleCountEl) {
|
if (aprsVisibleCountEl) {
|
||||||
aprsVisibleCountEl.textContent = `${visible.length} shown`;
|
aprsVisibleCountEl.textContent = `${visible.length} shown`;
|
||||||
}
|
}
|
||||||
if (aprsLatestSeenEl) {
|
if (aprsLatestSeenEl) {
|
||||||
const latest = aprsPacketHistory[0];
|
const latest = rigPackets[0];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
aprsLatestSeenEl.textContent = "No packets yet";
|
aprsLatestSeenEl.textContent = "No packets yet";
|
||||||
} else {
|
} else {
|
||||||
@@ -259,11 +267,12 @@ function normalizeServerAprsPacket(pkt: AprsPacket): AprsPacket {
|
|||||||
|
|
||||||
function onServerAprsBatch(packets: AprsPacket[]): void {
|
function onServerAprsBatch(packets: AprsPacket[]): void {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
|
||||||
const normalized: AprsPacket[] = [];
|
const normalized: AprsPacket[] = [];
|
||||||
let hasCrcOk = false;
|
let hasCrcOk = false;
|
||||||
for (const pkt of packets) {
|
for (const pkt of packets) {
|
||||||
const next = normalizeServerAprsPacket(pkt);
|
const next = normalizeServerAprsPacket(pkt);
|
||||||
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
|
if (aprsStatus && isActiveRigDecode(next.rig_id)) aprsStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
@@ -327,8 +336,9 @@ if (aprsFilterInput) {
|
|||||||
|
|
||||||
// --- Server-side APRS decode handler ---
|
// --- Server-side APRS decode handler ---
|
||||||
function onServerAprs(pkt: AprsPacket): void {
|
function onServerAprs(pkt: AprsPacket): void {
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
const packet = normalizeServerAprsPacket(pkt);
|
||||||
addAprsPacket(normalizeServerAprsPacket(pkt));
|
if (aprsStatus && isActiveRigDecode(packet.rig_id)) aprsStatus.textContent = "Receiving";
|
||||||
|
addAprsPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
@@ -339,6 +349,7 @@ renderAprsHistory();
|
|||||||
restore: onServerAprsBatch,
|
restore: onServerAprsBatch,
|
||||||
reset: resetAprsHistoryView,
|
reset: resetAprsHistoryView,
|
||||||
prune: pruneAprsHistoryView,
|
prune: pruneAprsHistoryView,
|
||||||
|
rerender: () => { updateAprsBar(); renderAprsHistory(); },
|
||||||
// Oldest first, so station tracks are rebuilt in the order they happened.
|
// Oldest first, so station tracks are rebuilt in the order they happened.
|
||||||
syncMap: () => { for (const entry of [...aprsPacketHistory].reverse()) plotAprsPacket(entry); },
|
syncMap: () => { for (const entry of [...aprsPacketHistory].reverse()) plotAprsPacket(entry); },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -432,8 +432,13 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
|
|||||||
|
|
||||||
// --- Server-side CW decode handler ---
|
// --- Server-side CW decode handler ---
|
||||||
function onServerCw(evt: CwEvent): void {
|
function onServerCw(evt: CwEvent): void {
|
||||||
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
|
// The panel is a single stream of copied text: characters from a background
|
||||||
if (evt.text && cwOutputEl) {
|
// rig do not belong in it at all, and interleaving them would make both
|
||||||
|
// rigs unreadable. The bar histories below keep every rig, each on its own
|
||||||
|
// line, and pick out the selected rig when they paint.
|
||||||
|
const forSelectedRig = isActiveRigDecode(evt.rig_id ?? null);
|
||||||
|
if (cwStatusEl && forSelectedRig) cwStatusEl.textContent = "Receiving";
|
||||||
|
if (evt.text && cwOutputEl && forSelectedRig) {
|
||||||
// Append decoded text to output
|
// Append decoded text to output
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 10000 || evt.text === "\n") {
|
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 10000 || evt.text === "\n") {
|
||||||
@@ -475,10 +480,10 @@ function onServerCw(evt: CwEvent): void {
|
|||||||
}
|
}
|
||||||
updateCwBar();
|
updateCwBar();
|
||||||
}
|
}
|
||||||
if (cwSignalIndicator) {
|
if (cwSignalIndicator && forSelectedRig) {
|
||||||
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
|
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
|
||||||
}
|
}
|
||||||
if (!cwAutoInput || cwAutoInput.checked) {
|
if (forSelectedRig && (!cwAutoInput || cwAutoInput.checked)) {
|
||||||
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
|
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
|
||||||
cwWpmInput.value = String(clampCwWpm(evt.wpm));
|
cwWpmInput.value = String(clampCwWpm(evt.wpm));
|
||||||
}
|
}
|
||||||
@@ -506,6 +511,13 @@ cwWindow.trxPluginRuntime.registerDecoder({
|
|||||||
onMessage: onServerCw,
|
onMessage: onServerCw,
|
||||||
restore: restoreCwHistory,
|
restore: restoreCwHistory,
|
||||||
reset: resetCwHistoryView,
|
reset: resetCwHistoryView,
|
||||||
|
// The copied text of a rig that is no longer on screen cannot be unpicked
|
||||||
|
// from the pane, so the switch starts the new rig's stream from empty.
|
||||||
|
rerender: () => {
|
||||||
|
if (cwOutputEl) cwOutputEl.innerHTML = "";
|
||||||
|
cwLastAppendTime = 0;
|
||||||
|
updateCwBar();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { hostCore } from "./host.js";
|
import { hostCore } from "./host.js";
|
||||||
|
|
||||||
import { isActiveRigDecode } from "./active-rig.js";
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
||||||
|
|
||||||
export type FtxDecoderId = "ft2" | "ft4" | "ft8";
|
export type FtxDecoderId = "ft2" | "ft4" | "ft8";
|
||||||
@@ -229,9 +229,12 @@ export function initializeFtxDecoder(config: FtxConfig): void {
|
|||||||
const render = (): void => {
|
const render = (): void => {
|
||||||
prune();
|
prune();
|
||||||
if (!messagesElement) return;
|
if (!messagesElement) return;
|
||||||
|
// The history holds every rig's decodes, for the map; the panel is about
|
||||||
|
// the rig the operator is working.
|
||||||
|
const rigMessages = forActiveRig(history);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const message of history) {
|
for (const message of rigMessages) {
|
||||||
if (count >= 200) break;
|
if (count >= 200) break;
|
||||||
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
|
||||||
fragment.appendChild(renderRow(message));
|
fragment.appendChild(renderRow(message));
|
||||||
@@ -267,7 +270,10 @@ export function initializeFtxDecoder(config: FtxConfig): void {
|
|||||||
};
|
};
|
||||||
const receiveBatch = (messages: FtxMessage[]): void => {
|
const receiveBatch = (messages: FtxMessage[]): void => {
|
||||||
if (messages.length === 0) return;
|
if (messages.length === 0) return;
|
||||||
if (status) status.textContent = "Receiving";
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
|
if (status && messages.some((message) => isActiveRigDecode(message.rig_id))) {
|
||||||
|
status.textContent = "Receiving";
|
||||||
|
}
|
||||||
history = messages.map(normalize).reverse().concat(history);
|
history = messages.map(normalize).reverse().concat(history);
|
||||||
prune();
|
prune();
|
||||||
bridge.setFt8FamilyBarDecoder?.(id);
|
bridge.setFt8FamilyBarDecoder?.(id);
|
||||||
@@ -305,6 +311,7 @@ export function initializeFtxDecoder(config: FtxConfig): void {
|
|||||||
restore: receiveBatch,
|
restore: receiveBatch,
|
||||||
prune: () => { prune(); render(); },
|
prune: () => { prune(); render(); },
|
||||||
reset,
|
reset,
|
||||||
|
rerender: () => { bridge.updateFt8Bar?.(); render(); },
|
||||||
});
|
});
|
||||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { hostCore, hostState } from "./host.js";
|
import { hostCore, hostState } from "./host.js";
|
||||||
|
|
||||||
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import {
|
import {
|
||||||
aprsAgeText,
|
aprsAgeText,
|
||||||
aprsPacketCategory,
|
aprsPacketCategory,
|
||||||
@@ -90,23 +91,31 @@ function hfAprsFilterMatch(pkt: AprsPacket): boolean {
|
|||||||
return haystack.includes(hfAprsFilterText);
|
return haystack.includes(hfAprsFilterText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What the selected rig heard. The history holds every rig's traffic, for
|
||||||
|
* the map; the panel is about the rig the operator is working. */
|
||||||
|
function hfAprsRigPackets(): AprsPacket[] {
|
||||||
|
return forActiveRig(hfAprsPacketHistory);
|
||||||
|
}
|
||||||
|
|
||||||
function hfAprsVisiblePackets(): AprsPacket[] {
|
function hfAprsVisiblePackets(): AprsPacket[] {
|
||||||
const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
|
const rigPackets = hfAprsRigPackets();
|
||||||
|
const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(rigPackets) : rigPackets;
|
||||||
return packets.filter(hfAprsFilterMatch);
|
return packets.filter(hfAprsFilterMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapseHfAprsDuplicates = collapseAprsDuplicates;
|
const collapseHfAprsDuplicates = collapseAprsDuplicates;
|
||||||
|
|
||||||
function updateHfAprsSummary() {
|
function updateHfAprsSummary() {
|
||||||
|
const rigPackets = hfAprsRigPackets();
|
||||||
const visible = hfAprsVisiblePackets();
|
const visible = hfAprsVisiblePackets();
|
||||||
if (hfAprsTotalCountEl) {
|
if (hfAprsTotalCountEl) {
|
||||||
hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
|
hfAprsTotalCountEl.textContent = `${rigPackets.length} total`;
|
||||||
}
|
}
|
||||||
if (hfAprsVisibleCountEl) {
|
if (hfAprsVisibleCountEl) {
|
||||||
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
|
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
|
||||||
}
|
}
|
||||||
if (hfAprsLatestSeenEl) {
|
if (hfAprsLatestSeenEl) {
|
||||||
const latest = hfAprsPacketHistory[0];
|
const latest = rigPackets[0];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
hfAprsLatestSeenEl.textContent = "No packets yet";
|
hfAprsLatestSeenEl.textContent = "No packets yet";
|
||||||
} else {
|
} else {
|
||||||
@@ -192,10 +201,11 @@ function normalizeServerHfAprsPacket(pkt: AprsPacket): AprsPacket {
|
|||||||
|
|
||||||
function onServerHfAprsBatch(packets: AprsPacket[]): void {
|
function onServerHfAprsBatch(packets: AprsPacket[]): void {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
|
||||||
const normalized: AprsPacket[] = [];
|
const normalized: AprsPacket[] = [];
|
||||||
for (const pkt of packets) {
|
for (const pkt of packets) {
|
||||||
const next = normalizeServerHfAprsPacket(pkt);
|
const next = normalizeServerHfAprsPacket(pkt);
|
||||||
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
|
if (hfAprsStatus && isActiveRigDecode(next.rig_id)) hfAprsStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
@@ -266,8 +276,9 @@ if (hfAprsFilterInput) {
|
|||||||
|
|
||||||
// --- Server-side HF APRS decode handler ---
|
// --- Server-side HF APRS decode handler ---
|
||||||
function onServerHfAprs(pkt: AprsPacket): void {
|
function onServerHfAprs(pkt: AprsPacket): void {
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
const packet = normalizeServerHfAprsPacket(pkt);
|
||||||
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
if (hfAprsStatus && isActiveRigDecode(packet.rig_id)) hfAprsStatus.textContent = "Receiving";
|
||||||
|
addHfAprsPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
@@ -278,4 +289,5 @@ renderHfAprsHistory();
|
|||||||
restore: onServerHfAprsBatch,
|
restore: onServerHfAprsBatch,
|
||||||
reset: resetHfAprsHistoryView,
|
reset: resetHfAprsHistoryView,
|
||||||
prune: pruneHfAprsHistoryView,
|
prune: pruneHfAprsHistoryView,
|
||||||
|
rerender: renderHfAprsHistory,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export interface DecoderPlugin<TMessage = unknown> {
|
|||||||
restore?(messages: TMessage[]): void;
|
restore?(messages: TMessage[]): void;
|
||||||
reset?(): void;
|
reset?(): void;
|
||||||
prune?(): void;
|
prune?(): void;
|
||||||
|
/** Repaint the plugin's panel from what it already holds. Called when the
|
||||||
|
* selected rig changes: the panels show one rig's traffic, and the decodes
|
||||||
|
* behind them arrived long before the switch. */
|
||||||
|
rerender?(): void;
|
||||||
/** Replay everything the plugin is holding onto the map. Called when the map
|
/** Replay everything the plugin is holding onto the map. Called when the map
|
||||||
* module attaches, which can happen long after the decodes arrived. */
|
* module attaches, which can happen long after the decodes arrived. */
|
||||||
syncMap?(): void;
|
syncMap?(): void;
|
||||||
@@ -22,6 +26,7 @@ export interface TrxPluginRuntime {
|
|||||||
reset(id: string): boolean;
|
reset(id: string): boolean;
|
||||||
resetAll(): void;
|
resetAll(): void;
|
||||||
prune(id: string): boolean;
|
prune(id: string): boolean;
|
||||||
|
rerenderAll(): void;
|
||||||
syncMapAll(): void;
|
syncMapAll(): void;
|
||||||
clearQueued(): void;
|
clearQueued(): void;
|
||||||
hasDecoder(id: string): boolean;
|
hasDecoder(id: string): boolean;
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
|
|
||||||
import { hostCore } from "./host.js";
|
import { hostCore } from "./host.js";
|
||||||
|
|
||||||
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
interface SstvImage {
|
interface SstvImage {
|
||||||
|
rig_id?: string | null;
|
||||||
ts_ms?: number;
|
ts_ms?: number;
|
||||||
vis?: number;
|
vis?: number;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
@@ -32,6 +34,7 @@ interface SstvImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SstvProgress {
|
interface SstvProgress {
|
||||||
|
rig_id?: string | null;
|
||||||
state?: string;
|
state?: string;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
@@ -185,6 +188,9 @@ function paintRow(line: number, rgb: Uint8Array): void {
|
|||||||
// ── Server messages ─────────────────────────────────────────────────
|
// ── Server messages ─────────────────────────────────────────────────
|
||||||
|
|
||||||
function onProgress(msg: SstvProgress): void {
|
function onProgress(msg: SstvProgress): void {
|
||||||
|
// The live canvas draws one picture at a time: a background rig's rows would
|
||||||
|
// paint straight over the one being watched.
|
||||||
|
if (!isActiveRigDecode(msg.rig_id)) return;
|
||||||
if (msg.state) {
|
if (msg.state) {
|
||||||
if (sstvDom.status) sstvDom.status.textContent = msg.state;
|
if (sstvDom.status) sstvDom.status.textContent = msg.state;
|
||||||
// A state update carries the geometry; a row update carries only the row.
|
// A state update carries the geometry; a row update carries only the row.
|
||||||
@@ -208,6 +214,9 @@ function onImage(msg: SstvImage): void {
|
|||||||
if (sstvHistory.length > SSTV_MAX_IMAGES) sstvHistory.shift();
|
if (sstvHistory.length > SSTV_MAX_IMAGES) sstvHistory.shift();
|
||||||
pruneHistory();
|
pruneHistory();
|
||||||
|
|
||||||
|
// Kept in the history either way, so switching to that rig finds it there.
|
||||||
|
if (!isActiveRigDecode(image.rig_id)) return;
|
||||||
|
|
||||||
if (sstvDom.status) {
|
if (sstvDom.status) {
|
||||||
sstvDom.status.textContent = image.complete
|
sstvDom.status.textContent = image.complete
|
||||||
? `Received ${image.mode ?? "picture"}`
|
? `Received ${image.mode ?? "picture"}`
|
||||||
@@ -219,9 +228,15 @@ function onImage(msg: SstvImage): void {
|
|||||||
|
|
||||||
// ── Rendering ───────────────────────────────────────────────────────
|
// ── Rendering ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** The pictures the selected rig decoded, oldest first, as the history is kept. */
|
||||||
|
function sstvRigImages(): SstvImage[] {
|
||||||
|
return forActiveRig(sstvHistory);
|
||||||
|
}
|
||||||
|
|
||||||
function renderLatestCard(): void {
|
function renderLatestCard(): void {
|
||||||
if (!sstvDom.liveLatest) return;
|
if (!sstvDom.liveLatest) return;
|
||||||
const latest = sstvHistory[sstvHistory.length - 1];
|
const rigImages = sstvRigImages();
|
||||||
|
const latest = rigImages[rigImages.length - 1];
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
sstvDom.liveLatest.innerHTML = "";
|
sstvDom.liveLatest.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
@@ -245,10 +260,12 @@ function renderLatestCard(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filteredHistory(): SstvImage[] {
|
function filteredHistory(): SstvImage[] {
|
||||||
|
// Every rig's pictures are kept; the panel shows the selected rig's.
|
||||||
|
const rigImages = sstvRigImages();
|
||||||
const text = filterText.trim().toLowerCase();
|
const text = filterText.trim().toLowerCase();
|
||||||
const matching = text
|
const matching = text
|
||||||
? sstvHistory.filter((image) => (image.mode ?? "").toLowerCase().includes(text))
|
? rigImages.filter((image) => (image.mode ?? "").toLowerCase().includes(text))
|
||||||
: sstvHistory.slice();
|
: rigImages;
|
||||||
const newestFirst = (sstvDom.sortSelect?.value ?? "newest") === "newest";
|
const newestFirst = (sstvDom.sortSelect?.value ?? "newest") === "newest";
|
||||||
matching.sort((a, b) => (newestFirst ? 1 : -1) * ((b._tsMs ?? 0) - (a._tsMs ?? 0)));
|
matching.sort((a, b) => (newestFirst ? 1 : -1) * ((b._tsMs ?? 0) - (a._tsMs ?? 0)));
|
||||||
return matching;
|
return matching;
|
||||||
@@ -348,6 +365,7 @@ sstvWindow.trxPluginRuntime.registerDecoder({
|
|||||||
restore: restoreHistory,
|
restore: restoreHistory,
|
||||||
prune: renderHistoryTable,
|
prune: renderHistoryTable,
|
||||||
reset: resetHistoryView,
|
reset: resetHistoryView,
|
||||||
|
rerender: () => { renderLatestCard(); renderHistoryTable(); },
|
||||||
});
|
});
|
||||||
sstvWindow.trxPluginRuntime.registerDecoder({
|
sstvWindow.trxPluginRuntime.registerDecoder({
|
||||||
id: "sstv_progress",
|
id: "sstv_progress",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { hostCore } from "./host.js";
|
|||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
import { isActiveRigDecode } from "./active-rig.js";
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import type { PluginRuntimeWindow } from "./runtime-contract";
|
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||||
|
|
||||||
interface VdesMessage {
|
interface VdesMessage {
|
||||||
@@ -117,17 +117,24 @@ function vdesHexPreview(rawBytes: number[] | undefined): string {
|
|||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What the selected rig heard. The history holds every rig's traffic, for
|
||||||
|
* the map; the panel is about the rig the operator is working. */
|
||||||
|
function vdesRigMessages(): VdesMessage[] {
|
||||||
|
return forActiveRig(vdesMessageHistory);
|
||||||
|
}
|
||||||
|
|
||||||
function updateVdesSummary() {
|
function updateVdesSummary() {
|
||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
if (vdesChannelSummaryEl) {
|
if (vdesChannelSummaryEl) {
|
||||||
vdesChannelSummaryEl.textContent = currentVdesCenterText();
|
vdesChannelSummaryEl.textContent = currentVdesCenterText();
|
||||||
}
|
}
|
||||||
|
const rigMessages = vdesRigMessages();
|
||||||
if (vdesFrameCountEl) {
|
if (vdesFrameCountEl) {
|
||||||
const count = vdesMessageHistory.length;
|
const count = rigMessages.length;
|
||||||
vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
|
vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
|
||||||
}
|
}
|
||||||
if (vdesLatestSeenEl) {
|
if (vdesLatestSeenEl) {
|
||||||
const latest = vdesMessageHistory[0];
|
const latest = rigMessages[0];
|
||||||
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
|
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,7 +288,7 @@ function renderVdesHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const message of vdesMessageHistory) {
|
for (const message of vdesRigMessages()) {
|
||||||
fragment.appendChild(renderVdesRow(message));
|
fragment.appendChild(renderVdesRow(message));
|
||||||
}
|
}
|
||||||
vdesMessagesEl.replaceChildren(fragment);
|
vdesMessagesEl.replaceChildren(fragment);
|
||||||
@@ -312,10 +319,11 @@ function normalizeServerVdesMessage(msg: VdesMessage): VdesMessage {
|
|||||||
|
|
||||||
function onServerVdesBatch(messages: VdesMessage[]): void {
|
function onServerVdesBatch(messages: VdesMessage[]): void {
|
||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
|
||||||
const normalized: VdesMessage[] = [];
|
const normalized: VdesMessage[] = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerVdesMessage(msg);
|
const next = normalizeServerVdesMessage(msg);
|
||||||
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
|
if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
|
||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
next._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
@@ -357,8 +365,8 @@ function plotVdesMessage(msg: VdesMessage): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onServerVdes(msg: VdesMessage): void {
|
function onServerVdes(msg: VdesMessage): void {
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
|
||||||
const next = normalizeServerVdesMessage(msg);
|
const next = normalizeServerVdesMessage(msg);
|
||||||
|
if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
|
||||||
addVdesMessage(next);
|
addVdesMessage(next);
|
||||||
plotVdesMessage(next);
|
plotVdesMessage(next);
|
||||||
}
|
}
|
||||||
@@ -377,6 +385,7 @@ updateVdesSummary();
|
|||||||
restore: onServerVdesBatch,
|
restore: onServerVdesBatch,
|
||||||
reset: resetVdesHistoryView,
|
reset: resetVdesHistoryView,
|
||||||
prune: pruneVdesHistoryView,
|
prune: pruneVdesHistoryView,
|
||||||
|
rerender: () => { updateVdesBar(); renderVdesHistory(); },
|
||||||
// Oldest first, so tracks are rebuilt in the order they happened.
|
// Oldest first, so tracks are rebuilt in the order they happened.
|
||||||
syncMap: () => { for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry); },
|
syncMap: () => { for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry); },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
import { hostCore } from "./host.js";
|
import { hostCore } from "./host.js";
|
||||||
|
|
||||||
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
interface WefaxImage {
|
interface WefaxImage {
|
||||||
|
rig_id?: string | null;
|
||||||
ts_ms?: number;
|
ts_ms?: number;
|
||||||
ioc?: number;
|
ioc?: number;
|
||||||
lpm?: number;
|
lpm?: number;
|
||||||
@@ -20,6 +22,7 @@ interface WefaxImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WefaxProgress {
|
interface WefaxProgress {
|
||||||
|
rig_id?: string | null;
|
||||||
state?: string;
|
state?: string;
|
||||||
line_data?: string;
|
line_data?: string;
|
||||||
line_count?: number;
|
line_count?: number;
|
||||||
@@ -156,15 +159,21 @@ function paintLine(lineBytes: Uint8Array): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Live view: latest image card ────────────────────────────────────
|
// ── Live view: latest image card ────────────────────────────────────
|
||||||
|
/** The pictures the selected rig decoded, newest first. */
|
||||||
|
function wefaxRigImages(): WefaxImage[] {
|
||||||
|
return forActiveRig(wefaxImageHistory);
|
||||||
|
}
|
||||||
|
|
||||||
function renderWefaxLatestCard() {
|
function renderWefaxLatestCard() {
|
||||||
if (!wefaxDom.liveLatest) return;
|
if (!wefaxDom.liveLatest) return;
|
||||||
if (wefaxImageHistory.length === 0) {
|
const rigImages = wefaxRigImages();
|
||||||
|
if (rigImages.length === 0) {
|
||||||
wefaxDom.liveLatest.innerHTML =
|
wefaxDom.liveLatest.innerHTML =
|
||||||
'<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
'<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = wefaxImageHistory[0];
|
const img = rigImages[0];
|
||||||
if (!img) return;
|
if (!img) return;
|
||||||
const ts = img._ts || '--';
|
const ts = img._ts || '--';
|
||||||
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : '';
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : '';
|
||||||
@@ -193,7 +202,8 @@ function renderWefaxLatestCard() {
|
|||||||
|
|
||||||
// ── History view: table ─────────────────────────────────────────────
|
// ── History view: table ─────────────────────────────────────────────
|
||||||
function getWefaxFilteredHistory() {
|
function getWefaxFilteredHistory() {
|
||||||
let items = wefaxImageHistory;
|
// Every rig's pictures are kept; the panel shows the selected rig's.
|
||||||
|
let items = wefaxRigImages();
|
||||||
|
|
||||||
if (wefaxFilterText) {
|
if (wefaxFilterText) {
|
||||||
items = items.filter(function (i) {
|
items = items.filter(function (i) {
|
||||||
@@ -253,7 +263,7 @@ function renderWefaxHistoryTable() {
|
|||||||
wefaxDom.historyList.replaceChildren(fragment);
|
wefaxDom.historyList.replaceChildren(fragment);
|
||||||
|
|
||||||
if (wefaxDom.historyCount) {
|
if (wefaxDom.historyCount) {
|
||||||
const total = wefaxImageHistory.length;
|
const total = wefaxRigImages().length;
|
||||||
const shown = items.length;
|
const shown = items.length;
|
||||||
wefaxDom.historyCount.textContent =
|
wefaxDom.historyCount.textContent =
|
||||||
total === 0
|
total === 0
|
||||||
@@ -298,6 +308,9 @@ function addWefaxImage(msg: WefaxImage): void {
|
|||||||
|
|
||||||
// ── SSE event handlers (public API) ─────────────────────────────────
|
// ── SSE event handlers (public API) ─────────────────────────────────
|
||||||
function onServerWefaxProgress(msg: WefaxProgress): void {
|
function onServerWefaxProgress(msg: WefaxProgress): void {
|
||||||
|
// The live canvas draws one picture at a time: a background rig's lines
|
||||||
|
// would paint straight over the one being watched.
|
||||||
|
if (!isActiveRigDecode(msg.rig_id)) return;
|
||||||
// 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) {
|
||||||
@@ -331,6 +344,8 @@ function onServerWefaxProgress(msg: WefaxProgress): void {
|
|||||||
|
|
||||||
function onServerWefax(msg: WefaxImage): void {
|
function onServerWefax(msg: WefaxImage): void {
|
||||||
addWefaxImage(msg);
|
addWefaxImage(msg);
|
||||||
|
// Kept in the history either way, so switching to that rig finds it there.
|
||||||
|
if (!isActiveRigDecode(msg.rig_id)) return;
|
||||||
|
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
@@ -438,6 +453,7 @@ wefaxWindow.trxPluginRuntime.registerDecoder({
|
|||||||
restore: restoreWefaxHistory,
|
restore: restoreWefaxHistory,
|
||||||
prune: pruneWefaxHistoryView,
|
prune: pruneWefaxHistoryView,
|
||||||
reset: resetWefaxHistoryView,
|
reset: resetWefaxHistoryView,
|
||||||
|
rerender: () => { renderWefaxLatestCard(); renderWefaxHistoryTable(); },
|
||||||
});
|
});
|
||||||
wefaxWindow.trxPluginRuntime.registerDecoder({
|
wefaxWindow.trxPluginRuntime.registerDecoder({
|
||||||
id: "wefax_progress",
|
id: "wefax_progress",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { hostCore } from "./host.js";
|
import { hostCore } from "./host.js";
|
||||||
|
|
||||||
|
import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
|
||||||
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
import type { PluginRuntimeWindow } from "./runtime-contract.js";
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
@@ -15,6 +16,7 @@ interface WsprMessage {
|
|||||||
snr_db?: number | undefined;
|
snr_db?: number | undefined;
|
||||||
dt_s?: number | undefined;
|
dt_s?: number | undefined;
|
||||||
freq_hz?: number | undefined;
|
freq_hz?: number | undefined;
|
||||||
|
rig_id?: string | null | undefined;
|
||||||
receiver?: unknown;
|
receiver?: unknown;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -102,10 +104,12 @@ function renderWsprRow(msg: WsprMessage): HTMLDivElement {
|
|||||||
function renderWsprHistory(): void {
|
function renderWsprHistory(): void {
|
||||||
pruneWsprMessageHistory();
|
pruneWsprMessageHistory();
|
||||||
if (!wsprMessagesEl) return;
|
if (!wsprMessagesEl) return;
|
||||||
|
// The history holds every rig's decodes, for the map; the panel is about the
|
||||||
|
// rig the operator is working.
|
||||||
|
const rigMessages = forActiveRig(wsprMessageHistory);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (let i = 0; i < wsprMessageHistory.length; i += 1) {
|
for (const message of rigMessages) {
|
||||||
const message = wsprMessageHistory[i];
|
fragment.appendChild(renderWsprRow(message));
|
||||||
if (message) fragment.appendChild(renderWsprRow(message));
|
|
||||||
}
|
}
|
||||||
wsprMessagesEl.replaceChildren(fragment);
|
wsprMessagesEl.replaceChildren(fragment);
|
||||||
}
|
}
|
||||||
@@ -130,6 +134,7 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
|
|||||||
station,
|
station,
|
||||||
rfHz,
|
rfHz,
|
||||||
history: {
|
history: {
|
||||||
|
rig_id: msg.rig_id ?? null,
|
||||||
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
||||||
ts_ms: msg.ts_ms,
|
ts_ms: msg.ts_ms,
|
||||||
snr_db: msg.snr_db,
|
snr_db: msg.snr_db,
|
||||||
@@ -142,10 +147,11 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
|
|||||||
|
|
||||||
function onServerWsprBatch(messages: WsprMessage[]): void {
|
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";
|
|
||||||
const normalized: WsprMessage[] = [];
|
const normalized: WsprMessage[] = [];
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerWsprMessage(msg);
|
const next = normalizeServerWsprMessage(msg);
|
||||||
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
||||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
||||||
...msg,
|
...msg,
|
||||||
@@ -312,7 +318,7 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onServerWspr(msg: WsprMessage): void {
|
function onServerWspr(msg: WsprMessage): void {
|
||||||
if (wsprStatus) wsprStatus.textContent = "Receiving";
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) 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) {
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
||||||
@@ -330,4 +336,5 @@ wsprWindow.trxPluginRuntime.registerDecoder({
|
|||||||
restore: onServerWsprBatch,
|
restore: onServerWsprBatch,
|
||||||
prune: pruneWsprHistoryView,
|
prune: pruneWsprHistoryView,
|
||||||
reset: resetWsprHistoryView,
|
reset: resetWsprHistoryView,
|
||||||
|
rerender: renderWsprHistory,
|
||||||
});
|
});
|
||||||
|
|||||||
+157
-7
@@ -2,11 +2,12 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
// The mini views over the waterfall caption the spectrum below them, so they
|
// The radio page and the digital modes page both describe one rig: the one the
|
||||||
// show only what the rig on screen heard. The panels on the decoder tabs stay
|
// operator selected. Every rig a client is connected to feeds the same decode
|
||||||
// aggregate — every rig's traffic lands there — which is the distinction these
|
// stream, so both the mini views over the waterfall and the panels behind them
|
||||||
// tests pin down: a decode from a background rig belongs in the list and not in
|
// filter on the rig that heard each decode. The map is the exception and keeps
|
||||||
// the overlay.
|
// all of them — it has its own rig filter — which is why the histories these
|
||||||
|
// views read from are never pruned by rig.
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
@@ -65,12 +66,14 @@ async function runPlugin(entry, { window, document: doc, extras = {} }) {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
test("the APRS mini view keeps to the active rig while the panel lists both", async () => {
|
test("the APRS mini view and panel both keep to the active rig", async () => {
|
||||||
const overlay = new ElementFixture();
|
const overlay = new ElementFixture();
|
||||||
const packets = new ElementFixture();
|
const packets = new ElementFixture();
|
||||||
|
const totals = new ElementFixture();
|
||||||
const elements = new Map([
|
const elements = new Map([
|
||||||
["aprs-bar-overlay", overlay],
|
["aprs-bar-overlay", overlay],
|
||||||
["aprs-packets", packets],
|
["aprs-packets", packets],
|
||||||
|
["aprs-total-count", totals],
|
||||||
["mode", new ElementFixture("PKT")],
|
["mode", new ElementFixture("PKT")],
|
||||||
]);
|
]);
|
||||||
const window = {
|
const window = {
|
||||||
@@ -86,7 +89,8 @@ test("the APRS mini view keeps to the active rig while the panel lists both", as
|
|||||||
|
|
||||||
assert.match(overlay.innerHTML, /SP1AAA/, "the active rig's frame is missing from the mini view");
|
assert.match(overlay.innerHTML, /SP1AAA/, "the active rig's frame is missing from the mini view");
|
||||||
assert.doesNotMatch(overlay.innerHTML, /SP2BBB/, "a background rig's frame reached the mini view");
|
assert.doesNotMatch(overlay.innerHTML, /SP2BBB/, "a background rig's frame reached the mini view");
|
||||||
assert.equal(packets.children.length, 2, "the APRS panel dropped a frame it should still list");
|
assert.equal(packets.children.length, 1, "the APRS panel listed a background rig's frame");
|
||||||
|
assert.equal(totals.textContent, "1 total", `the APRS panel counted "${totals.textContent}"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the APRS mini view shows every frame until a rig is known", async () => {
|
test("the APRS mini view shows every frame until a rig is known", async () => {
|
||||||
@@ -196,3 +200,149 @@ test("the FT8 mini view keeps to the active rig", async () => {
|
|||||||
assert.match(overlay.innerHTML, /SP1AAA/);
|
assert.match(overlay.innerHTML, /SP1AAA/);
|
||||||
assert.doesNotMatch(overlay.innerHTML, /SP2BBB/, "a background rig's decode reached the mini view");
|
assert.doesNotMatch(overlay.innerHTML, /SP2BBB/, "a background rig's decode reached the mini view");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── The digital modes panels ───────────────────────────────────────────────
|
||||||
|
// The same rule one page over: a panel lists the selected rig's decodes only.
|
||||||
|
|
||||||
|
test("the AIS panel lists the active rig's vessels only", async () => {
|
||||||
|
const messages = new ElementFixture();
|
||||||
|
const count = new ElementFixture();
|
||||||
|
const elements = new Map([
|
||||||
|
["ais-messages", messages],
|
||||||
|
["ais-vessel-count", count],
|
||||||
|
["mode", new ElementFixture("AIS")],
|
||||||
|
]);
|
||||||
|
const window = {
|
||||||
|
...createHost({ state: { lastActiveRigId: "rig-a" } }),
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
aisMapAddVessel: () => {},
|
||||||
|
};
|
||||||
|
await runPlugin("ais", { window, document: createDocument(elements) });
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
window.trxPluginRuntime.dispatch("ais", { mmsi: 261000001, vessel_name: "NEARBY", channel: "A", ts_ms: now, rig_id: "rig-a" });
|
||||||
|
window.trxPluginRuntime.dispatch("ais", { mmsi: 261000002, vessel_name: "ELSEWHERE", channel: "A", ts_ms: now, rig_id: "rig-b" });
|
||||||
|
|
||||||
|
assert.equal(messages.children.length, 1, "the AIS panel listed a background rig's vessel");
|
||||||
|
assert.equal(count.textContent, "1 vessel", `the panel counted "${count.textContent}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the VDES panel lists the active rig's bursts only", async () => {
|
||||||
|
const messages = new ElementFixture();
|
||||||
|
const count = new ElementFixture();
|
||||||
|
const elements = new Map([
|
||||||
|
["vdes-messages", messages],
|
||||||
|
["vdes-frame-count", count],
|
||||||
|
["mode", new ElementFixture("VDES")],
|
||||||
|
]);
|
||||||
|
const window = {
|
||||||
|
...createHost({ state: { lastActiveRigId: "rig-a" } }),
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
vdesMapAddPoint: () => {},
|
||||||
|
};
|
||||||
|
await runPlugin("vdes", { window, document: createDocument(elements) });
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
window.trxPluginRuntime.dispatch("vdes", { callsign: "SP1AAA", bit_len: 120, ts_ms: now, rig_id: "rig-a" });
|
||||||
|
window.trxPluginRuntime.dispatch("vdes", { callsign: "SP2BBB", bit_len: 120, ts_ms: now, rig_id: "rig-b" });
|
||||||
|
|
||||||
|
assert.equal(messages.children.length, 1, "the VDES panel listed a background rig's burst");
|
||||||
|
assert.equal(count.textContent, "1 burst", `the panel counted "${count.textContent}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the FT8 panel lists the active rig's decodes only", async () => {
|
||||||
|
const messages = new ElementFixture();
|
||||||
|
const elements = new Map([
|
||||||
|
["ft8-messages", messages],
|
||||||
|
["mode", new ElementFixture("DIG")],
|
||||||
|
]);
|
||||||
|
const window = {
|
||||||
|
...createHost({ state: { lastActiveRigId: "rig-a" } }),
|
||||||
|
ft8BaseHz: 7_074_000,
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
mapAddLocator: () => {},
|
||||||
|
setInterval() { return 1; },
|
||||||
|
};
|
||||||
|
await runPlugin("ft8", { window, document: createDocument(elements) });
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
window.trxPluginRuntime.dispatch("ft8", { message: "CQ SP1AAA JO91", freq_hz: 500, ts_ms: now, rig_id: "rig-a" });
|
||||||
|
window.trxPluginRuntime.dispatch("ft8", { message: "CQ SP2BBB JO94", freq_hz: 800, ts_ms: now, rig_id: "rig-b" });
|
||||||
|
|
||||||
|
assert.equal(messages.children.length, 1, "the FT8 panel listed a background rig's decode");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the WSPR panel lists the active rig's spots only", async () => {
|
||||||
|
const messages = new ElementFixture();
|
||||||
|
const elements = new Map([
|
||||||
|
["wspr-messages", messages],
|
||||||
|
["mode", new ElementFixture("DIG")],
|
||||||
|
]);
|
||||||
|
const window = {
|
||||||
|
...createHost({ state: { lastActiveRigId: "rig-a" } }),
|
||||||
|
ft8BaseHz: 14_095_600,
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
mapAddLocator: () => {},
|
||||||
|
};
|
||||||
|
await runPlugin("wspr", {
|
||||||
|
window,
|
||||||
|
document: createDocument(elements),
|
||||||
|
extras: { setInterval: () => 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
window.trxPluginRuntime.dispatch("wspr", { message: "SP1AAA JO91 30", freq_hz: 1_500, ts_ms: now, rig_id: "rig-a" });
|
||||||
|
window.trxPluginRuntime.dispatch("wspr", { message: "SP2BBB JO94 27", freq_hz: 1_520, ts_ms: now, rig_id: "rig-b" });
|
||||||
|
|
||||||
|
assert.equal(messages.children.length, 1, "the WSPR panel listed a background rig's spot");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the CW pane copies the active rig only", async () => {
|
||||||
|
const output = new ElementFixture();
|
||||||
|
const elements = new Map([
|
||||||
|
["cw-output", output],
|
||||||
|
["cw-bar-overlay", new ElementFixture()],
|
||||||
|
["mode", new ElementFixture("CW")],
|
||||||
|
]);
|
||||||
|
const window = {
|
||||||
|
...createHost({ state: { lastActiveRigId: "rig-a" } }),
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
addEventListener() {},
|
||||||
|
};
|
||||||
|
await runPlugin("cw", { window, document: createDocument(elements) });
|
||||||
|
|
||||||
|
window.trxPluginRuntime.dispatch("cw", { text: "CQ ", wpm: 18, tone_hz: 700, rig_id: "rig-a" });
|
||||||
|
window.trxPluginRuntime.dispatch("cw", { text: "XX ", wpm: 22, tone_hz: 600, rig_id: "rig-b" });
|
||||||
|
window.trxPluginRuntime.dispatch("cw", { text: "SP1AAA", wpm: 18, tone_hz: 700, rig_id: "rig-a" });
|
||||||
|
|
||||||
|
const copied = output.children.map((line) => line.textContent).join("");
|
||||||
|
assert.equal(copied, "CQ SP1AAA", `the pane copied "${copied}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("switching rigs repaints the panels through the runtime", async () => {
|
||||||
|
const packets = new ElementFixture();
|
||||||
|
const elements = new Map([
|
||||||
|
["aprs-packets", packets],
|
||||||
|
["aprs-bar-overlay", new ElementFixture()],
|
||||||
|
["mode", new ElementFixture("PKT")],
|
||||||
|
]);
|
||||||
|
// A mutable host: the rig picker writes lastActiveRigId, and the panels read
|
||||||
|
// it as they paint.
|
||||||
|
const host = createHost({ state: { lastActiveRigId: "rig-a" } });
|
||||||
|
let activeRigId = "rig-a";
|
||||||
|
Object.defineProperty(host.trx.state, "lastActiveRigId", { get: () => activeRigId });
|
||||||
|
const window = { ...host, trxUi: { confirm: async () => true }, aprsMapAddStation: () => {} };
|
||||||
|
await runPlugin("aprs", { window, document: createDocument(elements) });
|
||||||
|
|
||||||
|
const frame = { dest_call: "APRS", packet_type: "position", crc_ok: true, info: "beacon" };
|
||||||
|
window.trxPluginRuntime.dispatch("aprs", { ...frame, src_call: "SP1AAA", rig_id: "rig-a" });
|
||||||
|
window.trxPluginRuntime.dispatch("aprs", { ...frame, src_call: "SP2BBB", rig_id: "rig-b" });
|
||||||
|
assert.equal(packets.children.length, 1);
|
||||||
|
|
||||||
|
activeRigId = "rig-b";
|
||||||
|
window.trxPluginRuntime.rerenderAll();
|
||||||
|
assert.equal(packets.children.length, 1, "the panel did not follow the switch");
|
||||||
|
const shown = packets.children.map((row) => row.innerHTML).join("");
|
||||||
|
assert.match(shown, /SP2BBB/, "the panel still shows the rig that was switched away from");
|
||||||
|
assert.doesNotMatch(shown, /SP1AAA/);
|
||||||
|
});
|
||||||
@@ -22,9 +22,9 @@ const BEACON = {
|
|||||||
packet_type: "position", crc_ok: true, lat: 54.35, lon: 18.65,
|
packet_type: "position", crc_ok: true, lat: 54.35, lon: 18.65,
|
||||||
symbol_table: "/", symbol_code: ">", rig_id: "rig-a",
|
symbol_table: "/", symbol_code: ">", rig_id: "rig-a",
|
||||||
};
|
};
|
||||||
// A second rig listening in the background, on its own band. Its traffic
|
// A second rig listening in the background, on its own band. Both pages
|
||||||
// belongs in the panels, which aggregate every rig, but not in the mini view,
|
// describe the rig on screen, so its traffic belongs in neither the panel nor
|
||||||
// which captions the spectrum of the rig on screen.
|
// the mini view — only on the map, which shows the whole station.
|
||||||
const OTHER_RIG_VESSEL = {
|
const OTHER_RIG_VESSEL = {
|
||||||
...VESSEL, mmsi: 244660001, vessel_name: "ELDERBERRY", callsign: "PBTY",
|
...VESSEL, mmsi: 244660001, vessel_name: "ELDERBERRY", callsign: "PBTY",
|
||||||
lat: 51.92, lon: 4.48, rig_id: "rig-b",
|
lat: 51.92, lon: 4.48, rig_id: "rig-b",
|
||||||
@@ -51,12 +51,13 @@ try {
|
|||||||
aisStatus: document.getElementById("ais-status")?.textContent ?? "",
|
aisStatus: document.getElementById("ais-status")?.textContent ?? "",
|
||||||
aprsStatus: document.getElementById("aprs-status")?.textContent ?? "",
|
aprsStatus: document.getElementById("aprs-status")?.textContent ?? "",
|
||||||
otherRig: document.getElementById("ais-messages")?.textContent.includes("ELDERBERRY") ?? false,
|
otherRig: document.getElementById("ais-messages")?.textContent.includes("ELDERBERRY") ?? false,
|
||||||
|
aisRows: document.getElementById("ais-messages")?.children.length ?? 0,
|
||||||
mapLoaded: !!window.trx.modules.map,
|
mapLoaded: !!window.trx.modules.map,
|
||||||
}));
|
}));
|
||||||
assert.equal(panels.mapLoaded, false, "the map module was loaded, so this proves nothing");
|
assert.equal(panels.mapLoaded, false, "the map module was loaded, so this proves nothing");
|
||||||
assert.ok(panels.ais > 0, `the AIS panel is empty (status: ${panels.aisStatus})`);
|
assert.ok(panels.ais > 0, `the AIS panel is empty (status: ${panels.aisStatus})`);
|
||||||
assert.ok(panels.aprs > 0, `the APRS panel is empty (status: ${panels.aprsStatus})`);
|
assert.ok(panels.aprs > 0, `the APRS panel is empty (status: ${panels.aprsStatus})`);
|
||||||
assert.equal(panels.otherRig, true, "the AIS panel dropped the background rig's vessel");
|
assert.equal(panels.otherRig, false, "the AIS panel listed a background rig's vessel");
|
||||||
|
|
||||||
// The mini view rides over the waterfall on the radio page.
|
// The mini view rides over the waterfall on the radio page.
|
||||||
await page.locator('.tab[data-tab="main"]').click();
|
await page.locator('.tab[data-tab="main"]').click();
|
||||||
@@ -102,8 +103,11 @@ try {
|
|||||||
: Object.keys(collection ?? {}).length);
|
: Object.keys(collection ?? {}).length);
|
||||||
return { ais: size(map?.aisMarkers), stations: size(map?.stationMarkers) };
|
return { ais: size(map?.aisMarkers), stations: size(map?.stationMarkers) };
|
||||||
});
|
});
|
||||||
assert.ok(markers.ais > 0, "the vessel never reached the map");
|
|
||||||
assert.ok(markers.stations > 0, "the APRS station never reached the map");
|
assert.ok(markers.stations > 0, "the APRS station never reached the map");
|
||||||
|
// The map is the whole station's view — the one place a background rig's
|
||||||
|
// traffic belongs — so both vessels are on it even though the panel and the
|
||||||
|
// mini view show only the selected rig's.
|
||||||
|
assert.equal(markers.ais, 2, `${markers.ais} of 2 vessels reached the map`);
|
||||||
|
|
||||||
assert.deepEqual(runtimeErrors, []);
|
assert.deepEqual(runtimeErrors, []);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user