[feat](trx-frontend-http): add dedicated WSPR plugin tab
Expose a WSPR subtab in the Plugins view with its own controls and message list, wire a dedicated wspr.js asset endpoint, and route WSPR decode events to the new panel. This makes WSPR visible in the HTTP frontend instead of reusing the FT8 panel for WSPR messages. Co-authored-by: Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -253,6 +253,7 @@ function render(update) {
|
||||
const aprsStatus = document.getElementById("aprs-status");
|
||||
const cwStatus = document.getElementById("cw-status");
|
||||
const ft8Status = document.getElementById("ft8-status");
|
||||
const wsprStatus = document.getElementById("wspr-status");
|
||||
if (aprsStatus && modeUpper !== "PKT" && aprsStatus.textContent === "Receiving") {
|
||||
aprsStatus.textContent = "Connected, listening for packets";
|
||||
}
|
||||
@@ -263,6 +264,10 @@ function render(update) {
|
||||
if (ft8Status && (!ft8Enabled || (modeUpper !== "DIG" && modeUpper !== "USB")) && ft8Status.textContent === "Receiving") {
|
||||
ft8Status.textContent = "Connected, listening for packets";
|
||||
}
|
||||
const wsprEnabled = !!update.wspr_decode_enabled;
|
||||
if (wsprStatus && (!wsprEnabled || (modeUpper !== "DIG" && modeUpper !== "USB")) && wsprStatus.textContent === "Receiving") {
|
||||
wsprStatus.textContent = "Connected, listening for packets";
|
||||
}
|
||||
if (update.status && typeof update.status.tx_en === "boolean") {
|
||||
lastTxEn = update.status.tx_en;
|
||||
pttBtn.textContent = update.status.tx_en ? "PTT On" : "PTT Off";
|
||||
@@ -283,6 +288,13 @@ function render(update) {
|
||||
ft8ToggleBtn.style.borderColor = ft8On ? "#00d17f" : "";
|
||||
ft8ToggleBtn.style.color = ft8On ? "#00d17f" : "";
|
||||
}
|
||||
const wsprToggleBtn = document.getElementById("wspr-decode-toggle-btn");
|
||||
if (wsprToggleBtn) {
|
||||
const wsprOn = !!update.wspr_decode_enabled;
|
||||
wsprToggleBtn.textContent = wsprOn ? "Disable WSPR" : "Enable WSPR";
|
||||
wsprToggleBtn.style.borderColor = wsprOn ? "#00d17f" : "";
|
||||
wsprToggleBtn.style.color = wsprOn ? "#00d17f" : "";
|
||||
}
|
||||
const cwAutoEl = document.getElementById("cw-auto");
|
||||
const cwWpmEl = document.getElementById("cw-wpm");
|
||||
const cwToneEl = document.getElementById("cw-tone");
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
<button class="sub-tab" data-subtab="map">Map</button>
|
||||
<button class="sub-tab" data-subtab="aprs">APRS</button>
|
||||
<button class="sub-tab" data-subtab="ft8">FT8</button>
|
||||
<button class="sub-tab" data-subtab="wspr">WSPR</button>
|
||||
<button class="sub-tab" data-subtab="cw">CW</button>
|
||||
</div>
|
||||
<div id="subtab-overview" class="sub-tab-panel">
|
||||
@@ -146,6 +147,12 @@
|
||||
Decodes FT8 messages from RX audio (DIG/USB only, toggle required).
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-item">
|
||||
<strong>WSPR Decoder</strong>
|
||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||
Decodes WSPR messages from RX audio (DIG/USB only, toggle required).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subtab-map" class="sub-tab-panel" style="display:none;">
|
||||
<div class="map-controls">
|
||||
@@ -177,6 +184,21 @@
|
||||
</div>
|
||||
<div id="ft8-messages"></div>
|
||||
</div>
|
||||
<div id="subtab-wspr" class="sub-tab-panel" style="display:none;">
|
||||
<div class="ft8-controls">
|
||||
<button id="wspr-decode-toggle-btn" type="button">Enable WSPR</button>
|
||||
<button id="wspr-clear-btn" type="button">Clear</button>
|
||||
<small id="wspr-status" style="color:var(--text-muted);">Waiting for server decode</small>
|
||||
</div>
|
||||
<div class="ft8-header">
|
||||
<span class="ft8-time">Time</span>
|
||||
<span class="ft8-snr">SNR</span>
|
||||
<span class="ft8-dt">DT</span>
|
||||
<span class="ft8-freq">RF</span>
|
||||
<span class="ft8-msg">Message</span>
|
||||
</div>
|
||||
<div id="wspr-messages"></div>
|
||||
</div>
|
||||
<div id="subtab-cw" class="sub-tab-panel" style="display:none;">
|
||||
<div class="cw-controls">
|
||||
<button id="cw-clear-btn" type="button">Clear</button>
|
||||
@@ -212,6 +234,7 @@
|
||||
<script src="/app.js"></script>
|
||||
<script src="/aprs.js"></script>
|
||||
<script src="/ft8.js"></script>
|
||||
<script src="/wspr.js"></script>
|
||||
<script src="/cw.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -149,15 +149,3 @@ window.onServerFt8 = function(msg) {
|
||||
message: msg.message,
|
||||
});
|
||||
};
|
||||
|
||||
// Reuse FT8 table rendering for WSPR until a dedicated WSPR panel is added.
|
||||
window.onServerWspr = function(msg) {
|
||||
ft8Status.textContent = "Receiving";
|
||||
addFt8Message({
|
||||
ts_ms: msg.ts_ms,
|
||||
snr_db: msg.snr_db,
|
||||
dt_s: msg.dt_s,
|
||||
freq_hz: msg.freq_hz,
|
||||
message: `[WSPR] ${msg.message}`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// --- WSPR Decoder Plugin (server-side decode) ---
|
||||
const wsprStatus = document.getElementById("wspr-status");
|
||||
const wsprMessagesEl = document.getElementById("wspr-messages");
|
||||
const WSPR_MAX_MESSAGES = 200;
|
||||
|
||||
function fmtWsprTime(tsMs) {
|
||||
if (!tsMs) return "--:--:--";
|
||||
return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
function renderWsprRow(msg) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
const snr = Number.isFinite(msg.snr_db) ? msg.snr_db.toFixed(1) : "--";
|
||||
const dt = Number.isFinite(msg.dt_s) ? msg.dt_s.toFixed(2) : "--";
|
||||
const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null;
|
||||
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) ? (baseHz + msg.freq_hz) : null;
|
||||
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
|
||||
const message = (msg.message || "").toString();
|
||||
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(msg.ts_ms)}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${escapeWsprHtml(message)}</span>`;
|
||||
return row;
|
||||
}
|
||||
|
||||
function addWsprMessage(msg) {
|
||||
wsprMessagesEl.prepend(renderWsprRow(msg));
|
||||
while (wsprMessagesEl.children.length > WSPR_MAX_MESSAGES) {
|
||||
wsprMessagesEl.removeChild(wsprMessagesEl.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeWsprHtml(input) {
|
||||
return input
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("\"", """);
|
||||
}
|
||||
|
||||
document.getElementById("wspr-decode-toggle-btn").addEventListener("click", async () => {
|
||||
try { await postPath("/toggle_wspr_decode"); } catch (e) { console.error("WSPR toggle failed", e); }
|
||||
});
|
||||
|
||||
document.getElementById("wspr-clear-btn").addEventListener("click", async () => {
|
||||
wsprMessagesEl.innerHTML = "";
|
||||
try { await postPath("/clear_wspr_decode"); } catch (e) { console.error("WSPR clear failed", e); }
|
||||
});
|
||||
|
||||
window.onServerWspr = function(msg) {
|
||||
wsprStatus.textContent = "Receiving";
|
||||
addWsprMessage({
|
||||
ts_ms: msg.ts_ms,
|
||||
snr_db: msg.snr_db,
|
||||
dt_s: msg.dt_s,
|
||||
freq_hz: msg.freq_hz,
|
||||
message: msg.message,
|
||||
});
|
||||
};
|
||||
@@ -441,6 +441,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(app_js)
|
||||
.service(aprs_js)
|
||||
.service(ft8_js)
|
||||
.service(wspr_js)
|
||||
.service(cw_js);
|
||||
}
|
||||
|
||||
@@ -502,6 +503,16 @@ async fn ft8_js() -> impl Responder {
|
||||
.body(status::FT8_JS)
|
||||
}
|
||||
|
||||
#[get("/wspr.js")]
|
||||
async fn wspr_js() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/javascript; charset=utf-8")
|
||||
.insert_header((header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"))
|
||||
.insert_header((header::PRAGMA, "no-cache"))
|
||||
.insert_header((header::EXPIRES, "0"))
|
||||
.body(status::WSPR_JS)
|
||||
}
|
||||
|
||||
#[get("/cw.js")]
|
||||
async fn cw_js() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
|
||||
@@ -10,6 +10,7 @@ pub const STYLE_CSS: &str = include_str!("../assets/web/style.css");
|
||||
pub const APP_JS: &str = include_str!("../assets/web/app.js");
|
||||
pub const APRS_JS: &str = include_str!("../assets/web/plugins/aprs.js");
|
||||
pub const FT8_JS: &str = include_str!("../assets/web/plugins/ft8.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 fn index_html() -> String {
|
||||
|
||||
Reference in New Issue
Block a user