[feat](trx-wxsat): add Meteor-M LRPT decoder and Weather Satellites frontend panel
Restructure trx-wxsat into noaa/ (APT) and lrpt/ (Meteor-M LRPT) submodules with shared crate base. Add QPSK demodulator, CCSDS CADU framer, MCU channel assembler for LRPT. Wire LRPT through full stack (core types, protocol, server decoder task, client). Add Weather Satellites sub-tab in Digital Modes with toggle buttons for NOAA APT and Meteor LRPT, descriptions, and image history. https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -215,6 +215,8 @@ function applyAuthRestrictions() {
|
||||
"ft4-decode-toggle-btn",
|
||||
"ft2-decode-toggle-btn",
|
||||
"wspr-decode-toggle-btn",
|
||||
"wxsat-decode-toggle-btn",
|
||||
"lrpt-decode-toggle-btn",
|
||||
"hf-aprs-decode-toggle-btn",
|
||||
"cw-auto",
|
||||
"settings-clear-ais-history",
|
||||
@@ -225,7 +227,8 @@ function applyAuthRestrictions() {
|
||||
"settings-clear-ft8-history",
|
||||
"settings-clear-ft4-history",
|
||||
"settings-clear-ft2-history",
|
||||
"settings-clear-wspr-history"
|
||||
"settings-clear-wspr-history",
|
||||
"settings-clear-wxsat-history"
|
||||
];
|
||||
pluginToggleBtns.forEach(id => {
|
||||
const btn = document.getElementById(id);
|
||||
@@ -3228,6 +3231,22 @@ function render(update) {
|
||||
hfAprsToggleBtn.style.borderColor = hfAprsOn ? "#00d17f" : "";
|
||||
hfAprsToggleBtn.style.color = hfAprsOn ? "#00d17f" : "";
|
||||
}
|
||||
const wxsatToggleBtn = document.getElementById("wxsat-decode-toggle-btn");
|
||||
if (wxsatToggleBtn) {
|
||||
const wxsatOn = !!update.wxsat_decode_enabled;
|
||||
wxsatToggleBtn.dataset.enabled = wxsatOn ? "true" : "false";
|
||||
wxsatToggleBtn.textContent = wxsatOn ? "Disable NOAA APT" : "Enable NOAA APT";
|
||||
wxsatToggleBtn.style.borderColor = wxsatOn ? "#00d17f" : "";
|
||||
wxsatToggleBtn.style.color = wxsatOn ? "#00d17f" : "";
|
||||
}
|
||||
const lrptToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
||||
if (lrptToggleBtn) {
|
||||
const lrptOn = !!update.lrpt_decode_enabled;
|
||||
lrptToggleBtn.dataset.enabled = lrptOn ? "true" : "false";
|
||||
lrptToggleBtn.textContent = lrptOn ? "Disable Meteor LRPT" : "Enable Meteor LRPT";
|
||||
lrptToggleBtn.style.borderColor = lrptOn ? "#00d17f" : "";
|
||||
lrptToggleBtn.style.color = lrptOn ? "#00d17f" : "";
|
||||
}
|
||||
const cwAutoEl = document.getElementById("cw-auto");
|
||||
const cwWpmEl = document.getElementById("cw-wpm");
|
||||
const cwToneEl = document.getElementById("cw-tone");
|
||||
@@ -3403,6 +3422,8 @@ function render(update) {
|
||||
["about-dec-wspr", update.wspr_decode_enabled],
|
||||
["about-dec-cw", update.cw_decode_enabled],
|
||||
["about-dec-aprs", update.aprs_decode_enabled || update.hf_aprs_decode_enabled],
|
||||
["about-dec-wxsat", update.wxsat_decode_enabled],
|
||||
["about-dec-lrpt", update.lrpt_decode_enabled],
|
||||
];
|
||||
for (const [id, enabled] of decMap) {
|
||||
const el = document.getElementById(id);
|
||||
@@ -8476,6 +8497,8 @@ function dispatchDecodeMessage(msg) {
|
||||
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
|
||||
if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg);
|
||||
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
|
||||
if (msg.type === "wxsat_image" && window.onServerWxsatImage) window.onServerWxsatImage(msg);
|
||||
if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg);
|
||||
}
|
||||
|
||||
function dispatchDecodeBatch(batch) {
|
||||
|
||||
@@ -515,6 +515,7 @@
|
||||
<button class="sub-tab" data-subtab="ft2">FT2</button>
|
||||
<button class="sub-tab" data-subtab="wspr">WSPR</button>
|
||||
<button class="sub-tab" data-subtab="rds">RDS</button>
|
||||
<button class="sub-tab" data-subtab="wxsat">Weather Satellites</button>
|
||||
</div>
|
||||
<div id="subtab-overview" class="sub-tab-panel">
|
||||
<div class="plugin-item">
|
||||
@@ -571,6 +572,12 @@
|
||||
Decodes Radio Data System (RDS) metadata from WFM broadcasts (57 kHz subcarrier).
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-item">
|
||||
<strong>Weather Satellite Decoder</strong>
|
||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||
Decodes NOAA APT (137 MHz FM) and Meteor-M LRPT (137 MHz QPSK) weather satellite imagery.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subtab-rds" class="sub-tab-panel" style="display:none;">
|
||||
<div class="rds-grid">
|
||||
@@ -794,6 +801,24 @@
|
||||
</div>
|
||||
<div id="cw-output"></div>
|
||||
</div>
|
||||
<div id="subtab-wxsat" class="sub-tab-panel" style="display:none;">
|
||||
<div class="ft8-controls">
|
||||
<button id="wxsat-decode-toggle-btn" type="button">Enable NOAA APT</button>
|
||||
<button id="lrpt-decode-toggle-btn" type="button">Enable Meteor LRPT</button>
|
||||
<small id="wxsat-status" style="color:var(--text-muted);">Waiting for satellite pass</small>
|
||||
</div>
|
||||
<div style="margin:0.5rem 0;">
|
||||
<div style="color:var(--text-muted); font-size:0.82rem; line-height:1.5;">
|
||||
<strong>NOAA APT</strong> — Automatic Picture Transmission from NOAA-15/18/19 (137 MHz FM).
|
||||
Dual-channel visible + infrared imagery at 4160 samples/sec with telemetry-based radiometric calibration.
|
||||
</div>
|
||||
<div style="color:var(--text-muted); font-size:0.82rem; line-height:1.5; margin-top:0.3rem;">
|
||||
<strong>Meteor-M LRPT</strong> — Low Rate Picture Transmission from Meteor-M N2-3/N2-4 (137 MHz QPSK at 72 kbps).
|
||||
Multi-channel CCSDS-framed imagery (APIDs 64–69) with RGB composite output.
|
||||
</div>
|
||||
</div>
|
||||
<div id="wxsat-images"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-map" class="tab-panel" style="display:none;">
|
||||
<div id="map-stage">
|
||||
@@ -1041,6 +1066,7 @@
|
||||
<button id="settings-clear-ft4-history" class="sch-write sch-reset-btn" type="button">Clear full FT4 history</button>
|
||||
<button id="settings-clear-ft2-history" class="sch-write sch-reset-btn" type="button">Clear full FT2 history</button>
|
||||
<button id="settings-clear-wspr-history" class="sch-write sch-reset-btn" type="button">Clear full WSPR history</button>
|
||||
<button id="settings-clear-wxsat-history" class="sch-write sch-reset-btn" type="button">Clear full Weather Sat history</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1113,6 +1139,8 @@
|
||||
<tr><td>WSPR</td><td id="about-dec-wspr" class="about-status-off">Off</td></tr>
|
||||
<tr><td>CW</td><td id="about-dec-cw" class="about-status-off">Off</td></tr>
|
||||
<tr><td>APRS</td><td id="about-dec-aprs" class="about-status-off">Off</td></tr>
|
||||
<tr><td>NOAA APT</td><td id="about-dec-wxsat" class="about-status-off">Off</td></tr>
|
||||
<tr><td>Meteor LRPT</td><td id="about-dec-lrpt" class="about-status-off">Off</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Integrations -->
|
||||
@@ -1203,6 +1231,7 @@
|
||||
<script src="/ft2.js"></script>
|
||||
<script src="/wspr.js"></script>
|
||||
<script src="/cw.js"></script>
|
||||
<script src="/wxsat.js"></script>
|
||||
<script src="/bookmarks.js"></script>
|
||||
<script src="/scheduler.js"></script>
|
||||
<script src="/background-decode.js"></script>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// --- Weather Satellite Decoder Plugin ---
|
||||
const wxsatStatus = document.getElementById("wxsat-status");
|
||||
const wxsatImagesEl = document.getElementById("wxsat-images");
|
||||
|
||||
let wxsatImageHistory = [];
|
||||
const WXSAT_MAX_IMAGES = 20;
|
||||
|
||||
function scheduleWxsatUi(key, job) {
|
||||
if (typeof window.trxScheduleUiFrameJob === "function") {
|
||||
window.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
|
||||
function renderWxsatImage(img) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "wxsat-image-card";
|
||||
card.style.cssText =
|
||||
"border:1px solid var(--border-color);border-radius:0.5rem;padding:0.5rem;margin-bottom:0.75rem;background:var(--bg-secondary);";
|
||||
|
||||
const ts = img._ts || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
const decoder = img._decoder || "unknown";
|
||||
const satellite = img.satellite || "";
|
||||
const channels = img.channels || "";
|
||||
const lines = img.line_count || img.mcu_count || 0;
|
||||
|
||||
let metaParts = [`<strong>${decoder === "lrpt" ? "Meteor LRPT" : "NOAA APT"}</strong>`];
|
||||
if (satellite) metaParts.push(satellite);
|
||||
if (channels) metaParts.push("ch " + channels);
|
||||
metaParts.push(lines + (decoder === "lrpt" ? " MCU rows" : " lines"));
|
||||
metaParts.push(ts);
|
||||
|
||||
card.innerHTML =
|
||||
`<div style="font-size:0.82rem;color:var(--text-muted);margin-bottom:0.35rem;">${metaParts.join(" · ")}</div>`;
|
||||
|
||||
if (img.path) {
|
||||
const link = document.createElement("a");
|
||||
link.href = img.path;
|
||||
link.target = "_blank";
|
||||
link.textContent = "Download image";
|
||||
link.style.cssText = "font-size:0.8rem;color:var(--accent);";
|
||||
card.appendChild(link);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderWxsatHistory() {
|
||||
if (!wxsatImagesEl) return;
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < wxsatImageHistory.length; i += 1) {
|
||||
fragment.appendChild(renderWxsatImage(wxsatImageHistory[i]));
|
||||
}
|
||||
wxsatImagesEl.replaceChildren(fragment);
|
||||
}
|
||||
|
||||
function addWxsatImage(img, decoder) {
|
||||
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
|
||||
img._tsMs = tsMs;
|
||||
img._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
img._decoder = decoder;
|
||||
|
||||
wxsatImageHistory.unshift(img);
|
||||
if (wxsatImageHistory.length > WXSAT_MAX_IMAGES) {
|
||||
wxsatImageHistory = wxsatImageHistory.slice(0, WXSAT_MAX_IMAGES);
|
||||
}
|
||||
scheduleWxsatUi("wxsat-history", () => renderWxsatHistory());
|
||||
}
|
||||
|
||||
// Server-dispatched callbacks
|
||||
window.onServerWxsatImage = function (msg) {
|
||||
if (wxsatStatus) wxsatStatus.textContent = "Image received (NOAA APT)";
|
||||
addWxsatImage(msg, "apt");
|
||||
};
|
||||
|
||||
window.onServerLrptImage = function (msg) {
|
||||
if (wxsatStatus) wxsatStatus.textContent = "Image received (Meteor LRPT)";
|
||||
addWxsatImage(msg, "lrpt");
|
||||
};
|
||||
|
||||
window.resetWxsatHistoryView = function () {
|
||||
wxsatImageHistory = [];
|
||||
if (wxsatImagesEl) wxsatImagesEl.innerHTML = "";
|
||||
};
|
||||
|
||||
// Toggle buttons
|
||||
const wxsatDecodeToggleBtn = document.getElementById("wxsat-decode-toggle-btn");
|
||||
wxsatDecodeToggleBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
await window.takeSchedulerControlForDecoderDisable?.(wxsatDecodeToggleBtn);
|
||||
await postPath("/toggle_wxsat_decode");
|
||||
} catch (e) {
|
||||
console.error("WXSAT toggle failed", e);
|
||||
}
|
||||
});
|
||||
|
||||
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
||||
lrptDecodeToggleBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
await window.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
|
||||
await postPath("/toggle_lrpt_decode");
|
||||
} catch (e) {
|
||||
console.error("LRPT toggle failed", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear history button
|
||||
document
|
||||
.getElementById("settings-clear-wxsat-history")
|
||||
?.addEventListener("click", async () => {
|
||||
try {
|
||||
await postPath("/clear_wxsat_decode");
|
||||
await postPath("/clear_lrpt_decode");
|
||||
window.resetWxsatHistoryView();
|
||||
} catch (e) {
|
||||
console.error("Weather satellite history clear failed", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderWxsatHistory();
|
||||
Reference in New Issue
Block a user