[feat](trx-rs): add FT4 decoder support
Reuse the existing ft8_lib C library (FTX_PROTOCOL_FT4) and FT8 decoder infrastructure to add FT4 decoding across the full stack. Changes: - trx-ft8: add protocol param to ft8_decoder_create; add Ft8Decoder::new_ft4() - trx-core: DecodedMessage::Ft4 variant, AUDIO_MSG_FT4_DECODE (0x14), ft4_decode_enabled/ft4_decode_reset_seq state, SetFt4DecodeEnabled/ ResetFt4Decoder commands, protocol mapping - trx-server: DecoderHistories::ft4, run_ft4_decoder (7.5s slots via now*2/15), run_background_ft4_decoder, history push/replay, decoder task spawn - trx-frontend-http: ft4_history in FrontendRuntimeContext, toggle/clear endpoints, /ft4.js route, bookmark/scheduler/background decode support, DecodeHistoryPayload ft4 field - web: ft4.js plugin (7.5s period timer, reuses FT8 CSS/map infra), FT4 subtab in index.html, app.js dispatch (onServerFt4/Batch, restoreFt4History), decode-history-worker HISTORY_GROUP_KEYS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -202,6 +202,8 @@ pub struct FrontendRuntimeContext {
|
||||
pub cw_history: Arc<Mutex<VecDeque<(Instant, CwEvent)>>>,
|
||||
/// FT8 decode history (timestamp, message)
|
||||
pub ft8_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
||||
/// FT4 decode history (timestamp, message)
|
||||
pub ft4_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
|
||||
/// WSPR decode history (timestamp, message)
|
||||
pub wspr_history: Arc<Mutex<VecDeque<(Instant, WsprMessage)>>>,
|
||||
/// Authentication tokens for HTTP-JSON frontend
|
||||
@@ -283,6 +285,7 @@ impl FrontendRuntimeContext {
|
||||
hf_aprs_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
cw_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft4_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
auth_tokens: HashSet::new(),
|
||||
sse_clients: Arc::new(AtomicUsize::new(0)),
|
||||
|
||||
@@ -214,11 +214,13 @@ function applyAuthRestrictions() {
|
||||
"ais-clear-btn",
|
||||
"vdes-clear-btn",
|
||||
"ft8-decode-toggle-btn",
|
||||
"ft4-decode-toggle-btn",
|
||||
"wspr-decode-toggle-btn",
|
||||
"hf-aprs-decode-toggle-btn",
|
||||
"cw-auto",
|
||||
"aprs-clear-btn",
|
||||
"ft8-clear-btn",
|
||||
"ft4-clear-btn",
|
||||
"wspr-clear-btn",
|
||||
"cw-clear-btn"
|
||||
];
|
||||
@@ -2847,6 +2849,13 @@ function render(update) {
|
||||
ft8ToggleBtn.style.borderColor = ft8On ? "#00d17f" : "";
|
||||
ft8ToggleBtn.style.color = ft8On ? "#00d17f" : "";
|
||||
}
|
||||
const ft4ToggleBtn = document.getElementById("ft4-decode-toggle-btn");
|
||||
if (ft4ToggleBtn) {
|
||||
const ft4On = !!update.ft4_decode_enabled;
|
||||
ft4ToggleBtn.textContent = ft4On ? "Disable FT4" : "Enable FT4";
|
||||
ft4ToggleBtn.style.borderColor = ft4On ? "#00d17f" : "";
|
||||
ft4ToggleBtn.style.color = ft4On ? "#00d17f" : "";
|
||||
}
|
||||
const wsprToggleBtn = document.getElementById("wspr-decode-toggle-btn");
|
||||
if (wsprToggleBtn) {
|
||||
const wsprOn = !!update.wspr_decode_enabled;
|
||||
@@ -7312,6 +7321,7 @@ function updateDecodeStatus(text) {
|
||||
const aprs = document.getElementById("aprs-status");
|
||||
const cw = document.getElementById("cw-status");
|
||||
const ft8 = document.getElementById("ft8-status");
|
||||
const ft4 = document.getElementById("ft4-status");
|
||||
setModeBoundDecodeStatus(ais, ["AIS"], "Select AIS mode to decode", text);
|
||||
const vdesText = text === "Connected, listening for packets" ? "Connected, listening for bursts" : text;
|
||||
setModeBoundDecodeStatus(vdes, ["VDES"], "Select VDES mode to decode", vdesText);
|
||||
@@ -7319,6 +7329,7 @@ function updateDecodeStatus(text) {
|
||||
const cwText = text === "Connected, listening for packets" ? "Connected, listening for CW" : text;
|
||||
setModeBoundDecodeStatus(cw, ["CW", "CWR"], "Select CW mode to decode", cwText);
|
||||
if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text;
|
||||
if (ft4 && ft4.textContent !== "Receiving") ft4.textContent = text;
|
||||
}
|
||||
function dispatchDecodeMessage(msg) {
|
||||
if (msg.type === "ais" && window.onServerAis) window.onServerAis(msg);
|
||||
@@ -7327,6 +7338,7 @@ function dispatchDecodeMessage(msg) {
|
||||
if (msg.type === "hf_aprs" && window.onServerHfAprs) window.onServerHfAprs(msg);
|
||||
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
|
||||
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
|
||||
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
|
||||
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
|
||||
}
|
||||
|
||||
@@ -7355,6 +7367,10 @@ function dispatchDecodeBatch(batch) {
|
||||
window.onServerFt8Batch(batch);
|
||||
return;
|
||||
}
|
||||
if (type === "ft4" && window.onServerFt4Batch) {
|
||||
window.onServerFt4Batch(batch);
|
||||
return;
|
||||
}
|
||||
if (type === "wspr" && window.onServerWsprBatch) {
|
||||
window.onServerWsprBatch(batch);
|
||||
return;
|
||||
@@ -7425,6 +7441,10 @@ function restoreDecodeHistoryGroup(kind, messages) {
|
||||
window.restoreFt8History(messages);
|
||||
return;
|
||||
}
|
||||
if (kind === "ft4" && window.restoreFt4History) {
|
||||
window.restoreFt4History(messages);
|
||||
return;
|
||||
}
|
||||
if (kind === "wspr" && window.restoreWsprHistory) {
|
||||
window.restoreWsprHistory(messages);
|
||||
return;
|
||||
@@ -7441,6 +7461,7 @@ function connectDecode() {
|
||||
if (window.resetAprsHistoryView) window.resetAprsHistoryView();
|
||||
if (window.resetCwHistoryView) window.resetCwHistoryView();
|
||||
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
||||
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
|
||||
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
|
||||
|
||||
// Buffer live messages until history fetch settles so history always appears
|
||||
@@ -7512,7 +7533,7 @@ function connectDecode() {
|
||||
|
||||
function totalDecodeHistoryMessages(groups) {
|
||||
if (!groups || typeof groups !== "object") return 0;
|
||||
return ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "wspr"]
|
||||
return ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "wspr"]
|
||||
.reduce((sum, key) => sum + (Array.isArray(groups[key]) ? groups[key].length : 0), 0);
|
||||
}
|
||||
|
||||
@@ -7523,7 +7544,7 @@ function connectDecode() {
|
||||
setDecodeHistoryReplayActive(true);
|
||||
updateHistoryReplayOverlay();
|
||||
}
|
||||
for (const kind of ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "wspr"]) {
|
||||
for (const kind of ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "wspr"]) {
|
||||
const messages = groups && Array.isArray(groups[kind]) ? groups[kind] : [];
|
||||
if (messages.length === 0) continue;
|
||||
for (let index = 0; index < messages.length; index += DECODE_HISTORY_WORKER_GROUP_LIMIT) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "wspr"];
|
||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "wspr"];
|
||||
|
||||
function decodeCborUint(view, bytes, state, additional) {
|
||||
const offset = state.offset;
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-aprs" value="aprs" /> APRS</label>
|
||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ais" value="ais" /> AIS</label>
|
||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft8" value="ft8" /> FT8</label>
|
||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-ft4" value="ft4" /> FT4</label>
|
||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-wspr" value="wspr" /> WSPR</label>
|
||||
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-hf-aprs" value="hf-aprs" /> HF APRS</label>
|
||||
</div>
|
||||
@@ -425,6 +426,7 @@
|
||||
<button class="sub-tab" data-subtab="hf-aprs">HF APRS</button>
|
||||
<button class="sub-tab" data-subtab="cw">CW</button>
|
||||
<button class="sub-tab" data-subtab="ft8">FT8</button>
|
||||
<button class="sub-tab" data-subtab="ft4">FT4</button>
|
||||
<button class="sub-tab" data-subtab="wspr">WSPR</button>
|
||||
<button class="sub-tab" data-subtab="rds">RDS</button>
|
||||
</div>
|
||||
@@ -634,6 +636,24 @@
|
||||
</div>
|
||||
<div id="ft8-messages"></div>
|
||||
</div>
|
||||
<div id="subtab-ft4" class="sub-tab-panel" style="display:none;">
|
||||
<div class="ft8-controls">
|
||||
<button id="ft4-decode-toggle-btn" type="button">Enable FT4</button>
|
||||
<button id="ft4-pause-btn" type="button">Pause</button>
|
||||
<button id="ft4-clear-btn" type="button">Clear</button>
|
||||
<input id="ft4-filter" class="ft8-filter" type="text" placeholder="Filter (e.g. CQ, DL4)" />
|
||||
<small id="ft4-status" style="color:var(--text-muted);">Waiting for server decode</small>
|
||||
<small id="ft4-period" style="color:var(--text-muted);">Next slot --s</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="ft4-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>
|
||||
@@ -910,6 +930,7 @@
|
||||
<script src="/aprs.js"></script>
|
||||
<script src="/hf-aprs.js"></script>
|
||||
<script src="/ft8.js"></script>
|
||||
<script src="/ft4.js"></script>
|
||||
<script src="/wspr.js"></script>
|
||||
<script src="/cw.js"></script>
|
||||
<script src="/bookmarks.js"></script>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// --- FT4 Decoder Plugin (server-side decode) ---
|
||||
// SPDX-FileCopyrightText: 2026 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
function ft8RenderMessage(message) {
|
||||
if (typeof renderFt8Message === "function") return renderFt8Message(message);
|
||||
if (typeof ft8EscapeHtml === "function") return ft8EscapeHtml(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
const ft4Status = document.getElementById("ft4-status");
|
||||
const ft4PeriodEl = document.getElementById("ft4-period");
|
||||
const ft4MessagesEl = document.getElementById("ft4-messages");
|
||||
const ft4FilterInput = document.getElementById("ft4-filter");
|
||||
const ft4PauseBtn = document.getElementById("ft4-pause-btn");
|
||||
const FT4_PERIOD_MS = 7500;
|
||||
let ft4FilterText = "";
|
||||
let ft4MessageHistory = [];
|
||||
let ft4Paused = false;
|
||||
let ft4BufferedWhilePaused = 0;
|
||||
|
||||
function currentFt4HistoryRetentionMs() {
|
||||
return typeof window.getDecodeHistoryRetentionMs === "function"
|
||||
? window.getDecodeHistoryRetentionMs()
|
||||
: 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
function pruneFt4MessageHistory() {
|
||||
const cutoffMs = Date.now() - currentFt4HistoryRetentionMs();
|
||||
ft4MessageHistory = ft4MessageHistory.filter((msg) => Number(msg?._tsMs ?? msg?.ts_ms) >= cutoffMs);
|
||||
}
|
||||
|
||||
function scheduleFt4Ui(key, job) {
|
||||
if (typeof window.trxScheduleUiFrameJob === "function") {
|
||||
window.trxScheduleUiFrameJob(key, job);
|
||||
return;
|
||||
}
|
||||
job();
|
||||
}
|
||||
|
||||
function scheduleFt4HistoryRender() { scheduleFt4Ui("ft4-history", () => renderFt4History()); }
|
||||
|
||||
function normalizeFt4DisplayFreqHz(freqHz) {
|
||||
const rawHz = Number(freqHz);
|
||||
if (!Number.isFinite(rawHz)) return null;
|
||||
const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null;
|
||||
if (Number.isFinite(baseHz) && baseHz > 0 && rawHz >= 0 && rawHz < 100000) {
|
||||
return baseHz + rawHz;
|
||||
}
|
||||
return rawHz;
|
||||
}
|
||||
|
||||
function updateFt4PeriodTimer() {
|
||||
if (!ft4PeriodEl) return;
|
||||
const nowMs = Date.now();
|
||||
const remaining = (FT4_PERIOD_MS - nowMs % FT4_PERIOD_MS) / 1000;
|
||||
ft4PeriodEl.textContent = `Next slot ${remaining.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
updateFt4PeriodTimer();
|
||||
setInterval(updateFt4PeriodTimer, 250);
|
||||
|
||||
function renderFt4Row(msg) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
const rawMessage = (msg.message || "").toString();
|
||||
row.dataset.message = rawMessage.toUpperCase();
|
||||
row.dataset.decoder = "ft4";
|
||||
row.dataset.storedFreqHz = Number.isFinite(msg.freq_hz) ? String(msg.freq_hz) : "";
|
||||
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 displayFreqHz = normalizeFt4DisplayFreqHz(msg.freq_hz);
|
||||
const freq = Number.isFinite(displayFreqHz) ? displayFreqHz.toFixed(0) : "--";
|
||||
const renderedMessage = ft8RenderMessage(rawMessage);
|
||||
const tsMs = msg._tsMs ?? msg.ts_ms;
|
||||
const timeStr = tsMs ? new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : "--:--:--";
|
||||
row.innerHTML = `<span class="ft8-time">${timeStr}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderedMessage}</span>`;
|
||||
return row;
|
||||
}
|
||||
|
||||
function updateFt4PauseUi() {
|
||||
if (!ft4PauseBtn) return;
|
||||
ft4PauseBtn.textContent = ft4Paused ? "Resume" : "Pause";
|
||||
ft4PauseBtn.classList.toggle("active", ft4Paused);
|
||||
}
|
||||
|
||||
function renderFt4History() {
|
||||
pruneFt4MessageHistory();
|
||||
if (!ft4MessagesEl || ft4Paused) { updateFt4PauseUi(); return; }
|
||||
const filter = ft4FilterText;
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < ft4MessageHistory.length; i++) {
|
||||
const msg = ft4MessageHistory[i];
|
||||
if (filter && !(msg.message || "").toString().toUpperCase().includes(filter)) continue;
|
||||
fragment.appendChild(renderFt4Row(msg));
|
||||
}
|
||||
ft4MessagesEl.replaceChildren(fragment);
|
||||
updateFt4PauseUi();
|
||||
}
|
||||
|
||||
function addFt4Message(msg) {
|
||||
msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||
ft4MessageHistory.unshift(msg);
|
||||
pruneFt4MessageHistory();
|
||||
if (ft4Paused) { ft4BufferedWhilePaused += 1; updateFt4PauseUi(); return; }
|
||||
scheduleFt4HistoryRender();
|
||||
}
|
||||
|
||||
function normalizeServerFt4Message(msg) {
|
||||
const raw = (msg.message || "").toString();
|
||||
const locatorDetails = typeof ft8ExtractLocatorDetails === "function" ? ft8ExtractLocatorDetails(raw) : [];
|
||||
const grids = locatorDetails.length > 0
|
||||
? locatorDetails.map((d) => d.grid)
|
||||
: (typeof ft8ExtractAllGrids === "function" ? ft8ExtractAllGrids(raw) : []);
|
||||
const station = typeof ft8ExtractLikelyCallsign === "function" ? ft8ExtractLikelyCallsign(raw) : null;
|
||||
const rfHz = normalizeFt4DisplayFreqHz(msg.freq_hz);
|
||||
return {
|
||||
raw, grids, station, rfHz, locatorDetails,
|
||||
history: {
|
||||
receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
|
||||
ts_ms: msg.ts_ms, snr_db: msg.snr_db, dt_s: msg.dt_s,
|
||||
freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz,
|
||||
message: msg.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.onServerFt4Batch = function(messages) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
if (ft4Status) ft4Status.textContent = ft4Paused ? "Paused" : "Receiving";
|
||||
const normalized = [];
|
||||
for (const msg of messages) {
|
||||
const next = normalizeServerFt4Message(msg);
|
||||
if (next.grids.length > 0 && window.ft8MapAddLocator) {
|
||||
window.ft8MapAddLocator(next.raw, next.grids, "ft8", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
|
||||
}
|
||||
next.history._tsMs = Number.isFinite(next.history?.ts_ms) ? Number(next.history.ts_ms) : Date.now();
|
||||
normalized.push(next.history);
|
||||
}
|
||||
normalized.reverse();
|
||||
ft4MessageHistory = normalized.concat(ft4MessageHistory);
|
||||
pruneFt4MessageHistory();
|
||||
if (ft4Paused) { ft4BufferedWhilePaused += messages.length; updateFt4PauseUi(); return; }
|
||||
scheduleFt4HistoryRender();
|
||||
};
|
||||
|
||||
window.restoreFt4History = function(messages) { window.onServerFt4Batch(messages); };
|
||||
window.pruneFt4HistoryView = function() { pruneFt4MessageHistory(); renderFt4History(); };
|
||||
|
||||
window.resetFt4HistoryView = function() {
|
||||
if (ft4MessagesEl) ft4MessagesEl.innerHTML = "";
|
||||
ft4MessageHistory = [];
|
||||
ft4BufferedWhilePaused = 0;
|
||||
renderFt4History();
|
||||
};
|
||||
|
||||
if (ft4FilterInput) {
|
||||
ft4FilterInput.addEventListener("input", () => {
|
||||
ft4FilterText = ft4FilterInput.value.trim().toUpperCase();
|
||||
renderFt4History();
|
||||
});
|
||||
}
|
||||
|
||||
if (ft4PauseBtn) {
|
||||
ft4PauseBtn.addEventListener("click", () => {
|
||||
ft4Paused = !ft4Paused;
|
||||
if (!ft4Paused) { ft4BufferedWhilePaused = 0; renderFt4History(); } else { updateFt4PauseUi(); }
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("ft4-decode-toggle-btn")?.addEventListener("click", async () => {
|
||||
try { await postPath("/toggle_ft4_decode"); } catch (e) { console.error("FT4 toggle failed", e); }
|
||||
});
|
||||
|
||||
document.getElementById("ft4-clear-btn")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await postPath("/clear_ft4_decode");
|
||||
window.resetFt4HistoryView();
|
||||
} catch (e) { console.error("FT4 clear failed", e); }
|
||||
});
|
||||
|
||||
window.onServerFt4 = function(msg) {
|
||||
if (ft4Status) ft4Status.textContent = ft4Paused ? "Paused" : "Receiving";
|
||||
const next = normalizeServerFt4Message(msg);
|
||||
if (next.grids.length > 0 && window.ft8MapAddLocator) {
|
||||
window.ft8MapAddLocator(next.raw, next.grids, "ft8", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
|
||||
}
|
||||
addFt4Message(next.history);
|
||||
};
|
||||
|
||||
updateFt4PauseUi();
|
||||
@@ -450,6 +450,7 @@ struct DecodeHistoryPayload {
|
||||
hf_aprs: Vec<trx_core::decode::AprsPacket>,
|
||||
cw: Vec<trx_core::decode::CwEvent>,
|
||||
ft8: Vec<trx_core::decode::Ft8Message>,
|
||||
ft4: Vec<trx_core::decode::Ft8Message>,
|
||||
wspr: Vec<trx_core::decode::WsprMessage>,
|
||||
}
|
||||
|
||||
@@ -461,6 +462,7 @@ impl DecodeHistoryPayload {
|
||||
+ self.hf_aprs.len()
|
||||
+ self.cw.len()
|
||||
+ self.ft8.len()
|
||||
+ self.ft4.len()
|
||||
+ self.wspr.len()
|
||||
}
|
||||
}
|
||||
@@ -474,6 +476,7 @@ fn collect_decode_history(context: &FrontendRuntimeContext) -> DecodeHistoryPayl
|
||||
hf_aprs: crate::server::audio::snapshot_hf_aprs_history(context),
|
||||
cw: crate::server::audio::snapshot_cw_history(context),
|
||||
ft8: crate::server::audio::snapshot_ft8_history(context),
|
||||
ft4: crate::server::audio::snapshot_ft4_history(context),
|
||||
wspr: crate::server::audio::snapshot_wspr_history(context),
|
||||
}
|
||||
}
|
||||
@@ -1002,6 +1005,15 @@ pub async fn toggle_ft8_decode(
|
||||
send_command(&rig_tx, RigCommand::SetFt8DecodeEnabled(!enabled)).await
|
||||
}
|
||||
|
||||
#[post("/toggle_ft4_decode")]
|
||||
pub async fn toggle_ft4_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let enabled = state.get_ref().borrow().ft4_decode_enabled;
|
||||
send_command(&rig_tx, RigCommand::SetFt4DecodeEnabled(!enabled)).await
|
||||
}
|
||||
|
||||
#[post("/toggle_wspr_decode")]
|
||||
pub async fn toggle_wspr_decode(
|
||||
state: web::Data<watch::Receiver<RigState>>,
|
||||
@@ -1020,6 +1032,15 @@ pub async fn clear_ft8_decode(
|
||||
send_command(&rig_tx, RigCommand::ResetFt8Decoder).await
|
||||
}
|
||||
|
||||
#[post("/clear_ft4_decode")]
|
||||
pub async fn clear_ft4_decode(
|
||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
crate::server::audio::clear_ft4_history(context.get_ref());
|
||||
send_command(&rig_tx, RigCommand::ResetFt4Decoder).await
|
||||
}
|
||||
|
||||
#[post("/clear_wspr_decode")]
|
||||
pub async fn clear_wspr_decode(
|
||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||
@@ -1502,6 +1523,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(set_cw_wpm)
|
||||
.service(set_cw_tone)
|
||||
.service(toggle_ft8_decode)
|
||||
.service(toggle_ft4_decode)
|
||||
.service(toggle_wspr_decode)
|
||||
.service(clear_ais_decode)
|
||||
.service(clear_vdes_decode)
|
||||
@@ -1509,6 +1531,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(clear_hf_aprs_decode)
|
||||
.service(clear_cw_decode)
|
||||
.service(clear_ft8_decode)
|
||||
.service(clear_ft4_decode)
|
||||
.service(clear_wspr_decode)
|
||||
.service(select_rig)
|
||||
// Bookmark CRUD
|
||||
@@ -1542,6 +1565,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(aprs_js)
|
||||
.service(hf_aprs_js)
|
||||
.service(ft8_js)
|
||||
.service(ft4_js)
|
||||
.service(wspr_js)
|
||||
.service(cw_js)
|
||||
.service(bookmarks_js)
|
||||
@@ -1715,6 +1739,13 @@ async fn ft8_js() -> impl Responder {
|
||||
.body(status::FT8_JS)
|
||||
}
|
||||
|
||||
#[get("/ft4.js")]
|
||||
async fn ft4_js() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8"))
|
||||
.body(status::FT4_JS)
|
||||
}
|
||||
|
||||
#[get("/wspr.js")]
|
||||
async fn wspr_js() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
@@ -1848,10 +1879,11 @@ async fn send_command_to_rig(
|
||||
|
||||
fn bookmark_decoder_state(
|
||||
bookmark: &crate::server::bookmarks::Bookmark,
|
||||
) -> (bool, bool, bool, bool) {
|
||||
) -> (bool, bool, bool, bool, bool) {
|
||||
let mut want_aprs = bookmark.mode.trim().eq_ignore_ascii_case("PKT");
|
||||
let mut want_hf_aprs = false;
|
||||
let mut want_ft8 = false;
|
||||
let mut want_ft4 = false;
|
||||
let mut want_wspr = false;
|
||||
|
||||
for decoder in bookmark
|
||||
@@ -1863,12 +1895,13 @@ fn bookmark_decoder_state(
|
||||
"aprs" => want_aprs = true,
|
||||
"hf-aprs" => want_hf_aprs = true,
|
||||
"ft8" => want_ft8 = true,
|
||||
"ft4" => want_ft4 = true,
|
||||
"wspr" => want_wspr = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(want_aprs, want_hf_aprs, want_ft8, want_wspr)
|
||||
(want_aprs, want_hf_aprs, want_ft8, want_ft4, want_wspr)
|
||||
}
|
||||
|
||||
fn bookmark_decoder_kinds(bookmark: &crate::server::bookmarks::Bookmark) -> Vec<String> {
|
||||
@@ -1880,7 +1913,7 @@ fn bookmark_decoder_kinds(bookmark: &crate::server::bookmarks::Bookmark) -> Vec<
|
||||
{
|
||||
if matches!(
|
||||
decoder.as_str(),
|
||||
"aprs" | "ais" | "ft8" | "wspr" | "hf-aprs"
|
||||
"aprs" | "ais" | "ft8" | "ft4" | "wspr" | "hf-aprs"
|
||||
) && !out.iter().any(|existing| existing == &decoder)
|
||||
{
|
||||
out.push(decoder);
|
||||
@@ -1935,11 +1968,12 @@ async fn apply_selected_channel(
|
||||
let Some(bookmark) = bookmark_store.get(bookmark_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (want_aprs, want_hf_aprs, want_ft8, want_wspr) = bookmark_decoder_state(&bookmark);
|
||||
let (want_aprs, want_hf_aprs, want_ft8, want_ft4, want_wspr) = bookmark_decoder_state(&bookmark);
|
||||
let desired = [
|
||||
RigCommand::SetAprsDecodeEnabled(want_aprs),
|
||||
RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs),
|
||||
RigCommand::SetFt8DecodeEnabled(want_ft8),
|
||||
RigCommand::SetFt4DecodeEnabled(want_ft4),
|
||||
RigCommand::SetWsprDecodeEnabled(want_wspr),
|
||||
];
|
||||
for cmd in desired {
|
||||
@@ -1987,6 +2021,7 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
|
||||
cw_wpm: state.cw_wpm,
|
||||
cw_tone_hz: state.cw_tone_hz,
|
||||
ft8_decode_enabled: state.ft8_decode_enabled,
|
||||
ft4_decode_enabled: state.ft4_decode_enabled,
|
||||
wspr_decode_enabled: state.wspr_decode_enabled,
|
||||
filter: state.filter.clone(),
|
||||
spectrum: None,
|
||||
|
||||
@@ -147,6 +147,16 @@ fn prune_ft8_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(I
|
||||
}
|
||||
}
|
||||
|
||||
fn prune_ft4_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(Instant, Ft8Message)>) {
|
||||
let cutoff = decode_history_cutoff(context);
|
||||
while let Some((ts, _)) = history.front() {
|
||||
if *ts >= cutoff {
|
||||
break;
|
||||
}
|
||||
history.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
fn prune_wspr_history(
|
||||
context: &FrontendRuntimeContext,
|
||||
history: &mut VecDeque<(Instant, WsprMessage)>,
|
||||
@@ -202,6 +212,15 @@ fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
prune_ft8_history(context, &mut history);
|
||||
}
|
||||
|
||||
fn record_ft4(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
let mut history = context
|
||||
.ft4_history
|
||||
.lock()
|
||||
.expect("ft4 history mutex poisoned");
|
||||
history.push_back((Instant::now(), msg));
|
||||
prune_ft4_history(context, &mut history);
|
||||
}
|
||||
|
||||
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
|
||||
let mut history = context
|
||||
.wspr_history
|
||||
@@ -280,6 +299,15 @@ pub fn snapshot_ft8_history(context: &FrontendRuntimeContext) -> Vec<Ft8Message>
|
||||
history.iter().map(|(_, msg)| msg.clone()).collect()
|
||||
}
|
||||
|
||||
pub fn snapshot_ft4_history(context: &FrontendRuntimeContext) -> Vec<Ft8Message> {
|
||||
let mut history = context
|
||||
.ft4_history
|
||||
.lock()
|
||||
.expect("ft4 history mutex poisoned");
|
||||
prune_ft4_history(context, &mut history);
|
||||
history.iter().map(|(_, msg)| msg.clone()).collect()
|
||||
}
|
||||
|
||||
pub fn snapshot_wspr_history(context: &FrontendRuntimeContext) -> Vec<WsprMessage> {
|
||||
let mut history = context
|
||||
.wspr_history
|
||||
@@ -337,6 +365,14 @@ pub fn clear_ft8_history(context: &FrontendRuntimeContext) {
|
||||
history.clear();
|
||||
}
|
||||
|
||||
pub fn clear_ft4_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.ft4_history
|
||||
.lock()
|
||||
.expect("ft4 history mutex poisoned");
|
||||
history.clear();
|
||||
}
|
||||
|
||||
pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.wspr_history
|
||||
@@ -374,6 +410,7 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
|
||||
DecodedMessage::HfAprs(pkt) => record_hf_aprs(&context, pkt),
|
||||
DecodedMessage::Cw(evt) => record_cw(&context, evt),
|
||||
DecodedMessage::Ft8(msg) => record_ft8(&context, msg),
|
||||
DecodedMessage::Ft4(msg) => record_ft4(&context, msg),
|
||||
DecodedMessage::Wspr(msg) => record_wspr(&context, msg),
|
||||
},
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::server::bookmarks::{Bookmark, BookmarkStore};
|
||||
use crate::server::scheduler::{SchedulerStatusMap, SharedSchedulerControlManager};
|
||||
use crate::server::vchan::{ClientChannel, ClientChannelManager};
|
||||
|
||||
const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "wspr", "hf-aprs"];
|
||||
const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "ft4", "wspr", "hf-aprs"];
|
||||
const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel";
|
||||
const VISIBLE_CHANNEL_KIND_NAME: &str = "VirtualChannel";
|
||||
|
||||
|
||||
@@ -672,6 +672,7 @@ async fn apply_scheduler_decoders(
|
||||
let mut want_aprs = bookmark.mode.trim().eq_ignore_ascii_case("PKT");
|
||||
let mut want_hf_aprs = false;
|
||||
let mut want_ft8 = false;
|
||||
let mut want_ft4 = false;
|
||||
let mut want_wspr = false;
|
||||
|
||||
let mut update_from = |bm: &crate::server::bookmarks::Bookmark| {
|
||||
@@ -680,6 +681,7 @@ async fn apply_scheduler_decoders(
|
||||
"aprs" => want_aprs = true,
|
||||
"hf-aprs" => want_hf_aprs = true,
|
||||
"ft8" => want_ft8 = true,
|
||||
"ft4" => want_ft4 = true,
|
||||
"wspr" => want_wspr = true,
|
||||
_ => {}
|
||||
}
|
||||
@@ -695,6 +697,7 @@ async fn apply_scheduler_decoders(
|
||||
("APRS", RigCommand::SetAprsDecodeEnabled(want_aprs)),
|
||||
("HF APRS", RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs)),
|
||||
("FT8", RigCommand::SetFt8DecodeEnabled(want_ft8)),
|
||||
("FT4", RigCommand::SetFt4DecodeEnabled(want_ft4)),
|
||||
("WSPR", RigCommand::SetWsprDecodeEnabled(want_wspr)),
|
||||
];
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ pub const VDES_JS: &str = include_str!("../assets/web/plugins/vdes.js");
|
||||
pub const APRS_JS: &str = include_str!("../assets/web/plugins/aprs.js");
|
||||
pub const HF_APRS_JS: &str = include_str!("../assets/web/plugins/hf-aprs.js");
|
||||
pub const FT8_JS: &str = include_str!("../assets/web/plugins/ft8.js");
|
||||
pub const FT4_JS: &str = include_str!("../assets/web/plugins/ft4.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 BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
|
||||
|
||||
Reference in New Issue
Block a user