[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:
@@ -520,6 +520,7 @@ async fn async_init() -> DynResult<AppState> {
|
||||
}
|
||||
}
|
||||
DecodedMessage::WxsatImage(_) => {}
|
||||
DecodedMessage::LrptImage(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -1315,6 +1315,66 @@ pub async fn toggle_wspr_decode(
|
||||
.await
|
||||
}
|
||||
|
||||
#[post("/toggle_wxsat_decode")]
|
||||
pub async fn toggle_wxsat_decode(
|
||||
query: web::Query<RemoteQuery>,
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().wxsat_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetWxsatDecodeEnabled(!enabled),
|
||||
query.into_inner().remote,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[post("/toggle_lrpt_decode")]
|
||||
pub async fn toggle_lrpt_decode(
|
||||
query: web::Query<RemoteQuery>,
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().lrpt_decode_enabled;
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::SetLrptDecodeEnabled(!enabled),
|
||||
query.into_inner().remote,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[post("/clear_wxsat_decode")]
|
||||
pub async fn clear_wxsat_decode(
|
||||
query: web::Query<RemoteQuery>,
|
||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
crate::server::audio::clear_wxsat_history(context.get_ref());
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::ResetWxsatDecoder,
|
||||
query.into_inner().remote,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[post("/clear_lrpt_decode")]
|
||||
pub async fn clear_lrpt_decode(
|
||||
query: web::Query<RemoteQuery>,
|
||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
crate::server::audio::clear_lrpt_history(context.get_ref());
|
||||
send_command(
|
||||
&rig_tx,
|
||||
RigCommand::ResetLrptDecoder,
|
||||
query.into_inner().remote,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[post("/clear_ft8_decode")]
|
||||
pub async fn clear_ft8_decode(
|
||||
query: web::Query<RemoteQuery>,
|
||||
@@ -2029,6 +2089,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(toggle_ft4_decode)
|
||||
.service(toggle_ft2_decode)
|
||||
.service(toggle_wspr_decode)
|
||||
.service(toggle_wxsat_decode)
|
||||
.service(toggle_lrpt_decode)
|
||||
.service(clear_ais_decode)
|
||||
.service(clear_vdes_decode)
|
||||
.service(clear_aprs_decode)
|
||||
@@ -2038,6 +2100,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(clear_ft4_decode)
|
||||
.service(clear_ft2_decode)
|
||||
.service(clear_wspr_decode)
|
||||
.service(clear_wxsat_decode)
|
||||
.service(clear_lrpt_decode)
|
||||
.service(select_rig)
|
||||
// Bookmark CRUD
|
||||
.service(list_bookmarks)
|
||||
@@ -2076,6 +2140,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(ft2_js)
|
||||
.service(wspr_js)
|
||||
.service(cw_js)
|
||||
.service(wxsat_js)
|
||||
.service(bookmarks_js)
|
||||
.service(scheduler_js)
|
||||
.service(background_decode_js)
|
||||
@@ -2219,6 +2284,11 @@ async fn cw_js() -> impl Responder {
|
||||
no_cache_response("application/javascript; charset=utf-8", status::CW_JS)
|
||||
}
|
||||
|
||||
#[get("/wxsat.js")]
|
||||
async fn wxsat_js() -> impl Responder {
|
||||
no_cache_response("application/javascript; charset=utf-8", status::WXSAT_JS)
|
||||
}
|
||||
|
||||
#[get("/bookmarks.js")]
|
||||
async fn bookmarks_js() -> impl Responder {
|
||||
no_cache_response(
|
||||
|
||||
@@ -555,6 +555,7 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
|
||||
DecodedMessage::Ft2(msg) => record_ft2(&context, msg),
|
||||
DecodedMessage::Wspr(msg) => record_wspr(&context, msg),
|
||||
DecodedMessage::WxsatImage(_) => {}
|
||||
DecodedMessage::LrptImage(_) => {}
|
||||
},
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
|
||||
@@ -22,6 +22,7 @@ pub const FT4_JS: &str = include_str!("../assets/web/plugins/ft4.js");
|
||||
pub const FT2_JS: &str = include_str!("../assets/web/plugins/ft2.js");
|
||||
pub const WSPR_JS: &str = include_str!("../assets/web/plugins/wspr.js");
|
||||
pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
|
||||
pub const WXSAT_JS: &str = include_str!("../assets/web/plugins/wxsat.js");
|
||||
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
|
||||
pub const SCHEDULER_JS: &str = include_str!("../assets/web/plugins/scheduler.js");
|
||||
pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/plugins/background-decode.js");
|
||||
|
||||
Reference in New Issue
Block a user