[feat](trx-rs): add ft8 decoder

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-09 21:19:56 +01:00
parent 7bd1a70607
commit 1199ab85e9
206 changed files with 9613 additions and 5 deletions
@@ -248,12 +248,17 @@ function render(update) {
const modeUpper = update.status && update.status.mode ? normalizeMode(update.status.mode).toUpperCase() : "";
const aprsStatus = document.getElementById("aprs-status");
const cwStatus = document.getElementById("cw-status");
const ft8Status = document.getElementById("ft8-status");
if (aprsStatus && modeUpper !== "PKT" && aprsStatus.textContent === "Receiving") {
aprsStatus.textContent = "Connected, listening for packets";
}
if (cwStatus && modeUpper !== "CW" && modeUpper !== "CWR" && cwStatus.textContent === "Receiving") {
cwStatus.textContent = "Connected, listening for packets";
}
const ft8Enabled = !!update.ft8_decode_enabled;
if (ft8Status && (!ft8Enabled || (modeUpper !== "DIG" && modeUpper !== "USB")) && ft8Status.textContent === "Receiving") {
ft8Status.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";
@@ -267,6 +272,13 @@ function render(update) {
pttBtn.style.color = "";
}
}
const ft8ToggleBtn = document.getElementById("ft8-decode-toggle-btn");
if (ft8ToggleBtn) {
const ft8On = !!update.ft8_decode_enabled;
ft8ToggleBtn.textContent = ft8On ? "Disable FT8" : "Enable FT8";
ft8ToggleBtn.style.borderColor = ft8On ? "#00d17f" : "";
ft8ToggleBtn.style.color = ft8On ? "#00d17f" : "";
}
const cwAutoEl = document.getElementById("cw-auto");
const cwWpmEl = document.getElementById("cw-wpm");
const cwToneEl = document.getElementById("cw-tone");
@@ -1156,8 +1168,10 @@ let decodeConnected = false;
function updateDecodeStatus(text) {
const aprs = document.getElementById("aprs-status");
const cw = document.getElementById("cw-status");
const ft8 = document.getElementById("ft8-status");
if (aprs && aprs.textContent !== "Receiving") aprs.textContent = text;
if (cw && cw.textContent !== "Receiving") cw.textContent = text;
if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text;
}
function connectDecode() {
if (decodeSource) { decodeSource.close(); }
@@ -1171,6 +1185,7 @@ function connectDecode() {
const msg = JSON.parse(evt.data);
if (msg.type === "aprs" && window.onServerAprs) window.onServerAprs(msg);
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
} catch (e) {
// ignore parse errors
}
@@ -124,6 +124,7 @@
<button class="sub-tab active" data-subtab="overview">Overview</button>
<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="cw">CW</button>
</div>
<div id="subtab-overview" class="sub-tab-panel">
@@ -150,6 +151,14 @@
</div>
<div id="aprs-packets"></div>
</div>
<div id="subtab-ft8" class="sub-tab-panel" style="display:none;">
<div class="ft8-controls">
<button id="ft8-decode-toggle-btn" type="button">Enable FT8</button>
<button id="ft8-clear-btn" type="button">Clear</button>
<small id="ft8-status" style="color:var(--text-muted);">Waiting for server decode</small>
</div>
<div id="ft8-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>
@@ -184,6 +193,7 @@
</div>
<script src="/app.js"></script>
<script src="/aprs.js"></script>
<script src="/ft8.js"></script>
<script src="/cw.js"></script>
</body>
</html>
@@ -0,0 +1,47 @@
// --- FT8 Decoder Plugin (server-side decode) ---
const ft8Status = document.getElementById("ft8-status");
const ft8MessagesEl = document.getElementById("ft8-messages");
const FT8_MAX_MESSAGES = 200;
function fmtTime(tsMs) {
if (!tsMs) return "--:--:--";
return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function renderFt8Row(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 freq = Number.isFinite(msg.freq_hz) ? msg.freq_hz.toFixed(0) : "--";
row.innerHTML = `<span class="ft8-time">${fmtTime(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">${msg.message || ""}</span>`;
return row;
}
function addFt8Message(msg) {
ft8MessagesEl.prepend(renderFt8Row(msg));
while (ft8MessagesEl.children.length > FT8_MAX_MESSAGES) {
ft8MessagesEl.removeChild(ft8MessagesEl.lastChild);
}
}
document.getElementById("ft8-decode-toggle-btn").addEventListener("click", async () => {
try { await postPath("/toggle_ft8_decode"); } catch (e) { console.error("FT8 toggle failed", e); }
});
document.getElementById("ft8-clear-btn").addEventListener("click", async () => {
ft8MessagesEl.innerHTML = "";
try { await postPath("/clear_ft8_decode"); } catch (e) { console.error("FT8 clear failed", e); }
});
// --- Server-side FT8 decode handler ---
window.onServerFt8 = 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: msg.message,
});
};
@@ -223,6 +223,15 @@ small { color: var(--text-muted); }
.aprs-pos { color: var(--accent-green); text-decoration: none; margin-left: 0.3rem; font-size: 0.8rem; }
.aprs-pos:hover { text-decoration: underline; }
.aprs-byte { color: var(--accent-yellow); background: rgba(255, 214, 0, 0.12); border: 1px solid rgba(255, 214, 0, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-size: 0.78em; }
.ft8-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
#ft8-messages { max-height: 360px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85rem; padding: 0.35rem 0.5rem; }
.ft8-row { display: flex; gap: 0.6rem; line-height: 1.4; border-bottom: 1px solid var(--border); padding: 0.25rem 0; }
.ft8-row:last-child { border-bottom: none; }
.ft8-time { color: var(--text-muted); min-width: 4.6rem; }
.ft8-snr { color: var(--accent-yellow); min-width: 3.6rem; text-align: right; }
.ft8-dt { color: var(--text-muted); min-width: 3.6rem; text-align: right; }
.ft8-freq { color: var(--accent-green); min-width: 4.6rem; text-align: right; }
.ft8-msg { flex: 1; }
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.cw-config { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
@@ -321,6 +321,22 @@ pub async fn set_cw_tone(
send_command(&rig_tx, RigCommand::SetCwToneHz(query.tone_hz)).await
}
#[post("/toggle_ft8_decode")]
pub async fn toggle_ft8_decode(
state: web::Data<watch::Receiver<RigState>>,
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
) -> Result<HttpResponse, Error> {
let enabled = state.get_ref().borrow().ft8_decode_enabled;
send_command(&rig_tx, RigCommand::SetFt8DecodeEnabled(!enabled)).await
}
#[post("/clear_ft8_decode")]
pub async fn clear_ft8_decode(
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
) -> Result<HttpResponse, Error> {
send_command(&rig_tx, RigCommand::ResetFt8Decoder).await
}
#[post("/clear_aprs_decode")]
pub async fn clear_aprs_decode(
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
@@ -353,14 +369,17 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(set_cw_auto)
.service(set_cw_wpm)
.service(set_cw_tone)
.service(toggle_ft8_decode)
.service(clear_aprs_decode)
.service(clear_cw_decode)
.service(clear_ft8_decode)
.service(crate::server::audio::audio_ws)
.service(favicon)
.service(logo)
.service(style_css)
.service(app_js)
.service(aprs_js)
.service(ft8_js)
.service(cw_js);
}
@@ -406,6 +425,13 @@ async fn aprs_js() -> impl Responder {
.body(status::APRS_JS)
}
#[get("/ft8.js")]
async fn ft8_js() -> impl Responder {
HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8"))
.body(status::FT8_JS)
}
#[get("/cw.js")]
async fn cw_js() -> impl Responder {
HttpResponse::Ok()
@@ -488,6 +514,7 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
cw_auto: state.cw_auto,
cw_wpm: state.cw_wpm,
cw_tone_hz: state.cw_tone_hz,
ft8_decode_enabled: state.ft8_decode_enabled,
})
}
@@ -9,6 +9,7 @@ const INDEX_HTML: &str = include_str!("../assets/web/index.html");
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 CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
pub fn index_html() -> String {