[feat](trx-wefax): implement WEFAX decoder with full server and frontend integration

Pure Rust WEFAX (Weather Facsimile) decoder supporting 60/90/120/240 LPM,
IOC 288 and 576, with automatic APT tone detection and phase alignment.

Core DSP pipeline:
- Polyphase rational resampler (48k→11025 Hz)
- FM discriminator (Hilbert FIR + instantaneous frequency)
- Goertzel tone detector (300/450/675 Hz APT tones)
- Phase alignment via cross-correlation on phasing signal
- Line slicer with linear interpolation pixel clock recovery
- Image assembler with PNG encoding

State machine: Idle→StartDetected→Phasing→Receiving→Stopping

Server integration:
- WefaxMessage/WefaxProgress in trx-core DecodedMessage
- DecoderConfig, DecoderResetSeqs, RigCommand wefax variants
- DECODER_REGISTRY entry in trx-protocol
- DecoderHistories/DecoderLoggers wefax support
- run_wefax_decoder() async task in trx-server audio.rs
- History persistence in pickledb store

Frontend integration:
- wefax.js plugin with live canvas rendering and gallery
- HTML sub-tab with canvas, gallery, toggle/clear controls
- SSE dispatch for wefax/wefax_progress events
- Decode history worker and restore support
- Toggle/clear API endpoints

19 unit tests covering resampler, FM discriminator, tone detection,
phasing, line slicing, image encoding, and decoder state machine.

https://claude.ai/code/session_019eyxgx3LuhcFZ7T5tr2Trm
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-04-02 21:39:17 +00:00
committed by Stan Grams
parent d2db3d65bd
commit daa31fb6e5
40 changed files with 2292 additions and 40 deletions
+4 -1
View File
@@ -17,7 +17,8 @@ use uuid::Uuid;
use trx_core::audio::AudioStreamInfo;
use trx_core::decode::{
AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, VdesMessage, WsprMessage,
AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, VdesMessage, WefaxMessage,
WsprMessage,
};
use trx_core::rig::state::{RigSnapshot, SpectrumData};
use trx_core::{DynResult, RigRequest, RigState};
@@ -230,6 +231,7 @@ pub struct DecodeHistoryContext {
pub ft4: DecodeHistory<Ft8Message>,
pub ft2: DecodeHistory<Ft8Message>,
pub wspr: DecodeHistory<WsprMessage>,
pub wefax: DecodeHistory<WefaxMessage>,
}
impl Default for DecodeHistoryContext {
@@ -244,6 +246,7 @@ impl Default for DecodeHistoryContext {
ft4: Arc::new(Mutex::new(VecDeque::new())),
ft2: Arc::new(Mutex::new(VecDeque::new())),
wspr: Arc::new(Mutex::new(VecDeque::new())),
wefax: Arc::new(Mutex::new(VecDeque::new())),
}
}
}
@@ -5926,7 +5926,9 @@ function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg);
if (msg.type === "lrpt_progress" && window.onServerLrptProgress) window.onServerLrptProgress(msg);
if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress") {
if (msg.type === "wefax" && window.onServerWefax) window.onServerWefax(msg);
if (msg.type === "wefax_progress" && window.onServerWefaxProgress) window.onServerWefaxProgress(msg);
if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") {
window.trx.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
window.trx.map?.scheduleStatsRender();
}
@@ -5936,7 +5938,7 @@ function dispatchDecodeBatch(batch) {
if (!Array.isArray(batch) || batch.length === 0) return;
// Record statistics for every message in the batch regardless of dispatch path.
for (const msg of batch) {
if (msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress") {
if (msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") {
window.trx.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
}
}
@@ -6023,7 +6025,7 @@ function loadDecodeHistoryOnMainThread(onReady, onError) {
function restoreDecodeHistoryGroup(kind, messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
// Record statistics for restored history messages.
if (kind !== "lrpt_image" && kind !== "lrpt_progress") {
if (kind !== "lrpt_image" && kind !== "lrpt_progress" && kind !== "wefax" && kind !== "wefax_progress") {
for (const msg of messages) {
window.trx.map?.statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || undefined);
}
@@ -6065,6 +6067,10 @@ function restoreDecodeHistoryGroup(kind, messages) {
window.restoreWsprHistory(messages);
return;
}
if (kind === "wefax" && window.restoreWefaxHistory) {
window.restoreWefaxHistory(messages);
return;
}
}
function connectDecode() {
@@ -1,5 +1,5 @@
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr"];
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr", "wefax"];
function decodeCborUint(view, bytes, state, additional) {
const offset = state.offset;
@@ -538,6 +538,7 @@
<button class="sub-tab" data-subtab="wspr">WSPR</button>
<button class="sub-tab" data-subtab="rds">RDS</button>
<button class="sub-tab" data-subtab="sat">SAT</button>
<button class="sub-tab" data-subtab="wefax" id="subtab-wefax">WEFAX</button>
</div>
<div id="subtab-overview" class="sub-tab-panel">
<div class="plugin-item" data-decoder="ais">
@@ -600,6 +601,12 @@
Decodes Meteor-M LRPT (137 MHz QPSK) weather satellite imagery.
</div>
</div>
<div class="plugin-item" data-decoder="wefax">
<strong>WEFAX Decoder</strong>
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
Weather Facsimile &mdash; HF/satellite image reception (60/90/120/240 LPM)
</div>
</div>
</div>
<div id="subtab-rds" class="sub-tab-panel" style="display:none;">
<div class="rds-grid">
@@ -919,6 +926,22 @@
<small id="sat-pred-status" style="color:var(--text-muted);font-size:0.75rem;">Loading predictions&hellip;</small>
</div>
</div>
<div id="subtab-wefax" class="sub-tab-panel" style="display:none;">
<div class="ft8-controls">
<button id="wefax-decode-toggle-btn" type="button">Enable WEFAX</button>
<button id="wefax-clear-btn" type="button" style="margin-left:0.5rem; font-size:0.8rem;">Clear</button>
<small id="wefax-status" style="color:var(--text-muted);">Idle</small>
</div>
<div id="wefax-live-container" style="display:none; margin:0.5rem 0;">
<div style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.3rem;">
<strong>Receiving</strong>
<small id="wefax-live-info" style="color:var(--text-muted);"></small>
</div>
<canvas id="wefax-live-canvas" width="1809" height="800"
style="width:100%; image-rendering:pixelated; background:#000;"></canvas>
</div>
<div id="wefax-gallery" style="display:flex; flex-wrap:wrap; gap:0.5rem;"></div>
</div>
</div>
<div id="tab-map" class="tab-panel" data-tab="map" style="display:none;">
<template id="tmpl-map">
@@ -1476,6 +1499,7 @@
<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>Meteor LRPT</td><td id="about-dec-lrpt" class="about-status-off">Off</td></tr>
<tr id="about-dec-wefax"><td>WEFAX</td><td>Weather Facsimile decoder</td></tr>
</table>
</div>
<!-- Integrations -->
@@ -1562,7 +1586,7 @@
// Lazy plugin loader: loads plugin scripts when their tab/feature is first activated
(function() {
var pluginScripts = {
'digital-modes': ['/ft8.js', '/ft4.js', '/ft2.js', '/wspr.js', '/cw.js', '/background-decode.js', '/sat.js'],
'digital-modes': ['/ft8.js', '/ft4.js', '/ft2.js', '/wspr.js', '/cw.js', '/background-decode.js', '/sat.js', '/wefax.js'],
'map': ['/map-core.js', '/leaflet-ais-tracksymbol.js', '/ais.js', '/vdes.js', '/aprs.js', '/hf-aprs.js', '/sat.js', '/sat-scheduler.js'],
'statistics': ['/map-core.js'],
'bookmarks': ['/bookmarks.js'],
@@ -0,0 +1,193 @@
// ---------------------------------------------------------------------------
// wefax.js — WEFAX decoder plugin for trx-frontend-http
// ---------------------------------------------------------------------------
// --- DOM refs ---
var wefaxStatus = document.getElementById('wefax-status');
var wefaxLiveContainer= document.getElementById('wefax-live-container');
var wefaxLiveInfo = document.getElementById('wefax-live-info');
var wefaxLiveCanvas = document.getElementById('wefax-live-canvas');
var wefaxGallery = document.getElementById('wefax-gallery');
var wefaxToggleBtn = document.getElementById('wefax-decode-toggle-btn');
var wefaxClearBtn = document.getElementById('wefax-clear-btn');
// --- State ---
var wefaxImageHistory = [];
var wefaxLiveCtx = null;
var wefaxLiveLineCount = 0;
var wefaxLivePixelsPerLine = 1809;
// --- Helpers ---
function currentWefaxHistoryRetentionMs() {
return window.getDecodeHistoryRetentionMs ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1000;
}
function pruneWefaxHistory() {
var cutoff = Date.now() - currentWefaxHistoryRetentionMs();
wefaxImageHistory = wefaxImageHistory.filter(function (m) { return (m._tsMs || 0) > cutoff; });
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// --- Live canvas rendering ---
function resetLiveCanvas(pixelsPerLine) {
wefaxLivePixelsPerLine = pixelsPerLine;
wefaxLiveLineCount = 0;
wefaxLiveCanvas.width = pixelsPerLine;
wefaxLiveCanvas.height = 800;
wefaxLiveCtx = wefaxLiveCanvas.getContext('2d');
wefaxLiveCtx.fillStyle = '#000';
wefaxLiveCtx.fillRect(0, 0, wefaxLiveCanvas.width, wefaxLiveCanvas.height);
wefaxLiveContainer.style.display = '';
}
function paintLine(lineBytes) {
if (!wefaxLiveCtx) return;
var y = wefaxLiveLineCount;
if (y >= wefaxLiveCanvas.height) {
var old = wefaxLiveCtx.getImageData(0, 0, wefaxLiveCanvas.width, wefaxLiveCanvas.height);
wefaxLiveCanvas.height *= 2;
wefaxLiveCtx.putImageData(old, 0, 0);
}
var w = wefaxLivePixelsPerLine;
var imgData = wefaxLiveCtx.createImageData(w, 1);
var d = imgData.data;
for (var x = 0; x < w; x++) {
var v = x < lineBytes.length ? lineBytes[x] : 0;
var i = x * 4;
d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255;
}
wefaxLiveCtx.putImageData(imgData, 0, y);
wefaxLiveLineCount++;
}
// --- Gallery rendering ---
function renderGalleryThumbnail(msg) {
var card = document.createElement('div');
card.className = 'wefax-card';
card.style.cssText =
'border:1px solid var(--border-color); border-radius:4px; ' +
'padding:0.4rem; max-width:280px; cursor:pointer;';
var ts = msg._tsMs ? new Date(msg._tsMs).toLocaleString() : '\u2014';
var info = msg.ioc + ' IOC \u00b7 ' + msg.lpm + ' LPM \u00b7 ' + msg.line_count + ' lines';
if (msg.path) {
card.innerHTML =
'<img src="/images/' + escapeHtml(msg.path.split('/').pop()) + '"' +
' alt="WEFAX" loading="lazy"' +
' style="width:100%; image-rendering:pixelated;" />' +
'<div style="font-size:0.8rem; margin-top:0.2rem;">' + escapeHtml(ts) + '</div>' +
'<div style="font-size:0.75rem; color:var(--text-muted);">' + info + '</div>';
} else {
card.innerHTML =
'<div style="font-size:0.8rem;">' + escapeHtml(ts) + '</div>' +
'<div style="font-size:0.75rem; color:var(--text-muted);">' + info + '</div>';
}
return card;
}
function renderWefaxGallery() {
pruneWefaxHistory();
var frag = document.createDocumentFragment();
for (var i = 0; i < wefaxImageHistory.length; i++) {
frag.appendChild(renderGalleryThumbnail(wefaxImageHistory[i]));
}
wefaxGallery.innerHTML = '';
wefaxGallery.appendChild(frag);
}
function scheduleWefaxGalleryRender() {
if (window.trxScheduleUiFrameJob) {
window.trxScheduleUiFrameJob('wefax-gallery', renderWefaxGallery);
} else {
requestAnimationFrame(renderWefaxGallery);
}
}
// --- SSE event handlers (public API) ---
window.onServerWefaxProgress = function (msg) {
if (msg.line_count <= 1 || !wefaxLiveCtx) {
resetLiveCanvas(msg.pixels_per_line || 1809);
}
if (msg.line_data) {
var binary = atob(msg.line_data);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
paintLine(bytes);
}
if (wefaxLiveInfo) {
wefaxLiveInfo.textContent =
'Line ' + msg.line_count + ' \u00b7 ' + msg.ioc + ' IOC \u00b7 ' + msg.lpm + ' LPM';
}
if (wefaxStatus) {
wefaxStatus.textContent = 'Receiving \u2014 line ' + msg.line_count;
wefaxStatus.style.color = 'var(--text-accent)';
}
};
window.onServerWefax = function (msg) {
msg._tsMs = msg.ts_ms || Date.now();
wefaxImageHistory.unshift(msg);
pruneWefaxHistory();
scheduleWefaxGalleryRender();
if (wefaxLiveCtx && wefaxLiveLineCount > 0) {
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxLiveCanvas.width, wefaxLiveLineCount);
wefaxLiveCanvas.height = wefaxLiveLineCount;
wefaxLiveCtx.putImageData(trimmed, 0, 0);
}
if (wefaxStatus) {
wefaxStatus.textContent = 'Complete \u2014 ' + msg.line_count + ' lines';
wefaxStatus.style.color = '';
}
};
window.restoreWefaxHistory = function (messages) {
if (!messages || !messages.length) return;
for (var i = 0; i < messages.length; i++) {
messages[i]._tsMs = messages[i].ts_ms || Date.now();
}
wefaxImageHistory = messages.concat(wefaxImageHistory);
pruneWefaxHistory();
scheduleWefaxGalleryRender();
};
window.pruneWefaxHistoryView = function () {
pruneWefaxHistory();
scheduleWefaxGalleryRender();
};
window.resetWefaxHistoryView = function () {
wefaxImageHistory = [];
if (wefaxGallery) wefaxGallery.innerHTML = '';
if (wefaxLiveContainer) wefaxLiveContainer.style.display = 'none';
wefaxLiveCtx = null;
wefaxLiveLineCount = 0;
if (wefaxStatus) {
wefaxStatus.textContent = 'Idle';
wefaxStatus.style.color = '';
}
};
// --- Button handlers ---
if (wefaxClearBtn) {
wefaxClearBtn.addEventListener('click', function () {
fetch('/clear_wefax_decode', { method: 'POST' });
window.resetWefaxHistoryView();
});
}
@@ -55,6 +55,7 @@ define_gz_cache!(gz_ft2_js, status::FT2_JS, "ft2.js");
define_gz_cache!(gz_wspr_js, status::WSPR_JS, "wspr.js");
define_gz_cache!(gz_cw_js, status::CW_JS, "cw.js");
define_gz_cache!(gz_sat_js, status::SAT_JS, "sat.js");
define_gz_cache!(gz_wefax_js, status::WEFAX_JS, "wefax.js");
define_gz_cache!(gz_bookmarks_js, status::BOOKMARKS_JS, "bookmarks.js");
define_gz_cache!(gz_scheduler_js, status::SCHEDULER_JS, "scheduler.js");
define_gz_cache!(
@@ -325,6 +326,16 @@ pub(crate) async fn sat_js(req: HttpRequest) -> impl Responder {
)
}
#[get("/wefax.js")]
pub(crate) async fn wefax_js(req: HttpRequest) -> impl Responder {
let c = gz_wefax_js();
static_asset_response(
&req,
"application/javascript; charset=utf-8",
c,
)
}
#[get("/bookmarks.js")]
pub(crate) async fn bookmarks_js(req: HttpRequest) -> impl Responder {
let c = gz_bookmarks_js();
@@ -44,6 +44,7 @@ struct DecodeHistoryPayload {
ft4: Vec<trx_core::decode::Ft8Message>,
ft2: Vec<trx_core::decode::Ft8Message>,
wspr: Vec<trx_core::decode::WsprMessage>,
wefax: Vec<trx_core::decode::WefaxMessage>,
}
impl DecodeHistoryPayload {
@@ -57,6 +58,7 @@ impl DecodeHistoryPayload {
+ self.ft4.len()
+ self.ft2.len()
+ self.wspr.len()
+ self.wefax.len()
}
}
@@ -75,6 +77,7 @@ fn collect_decode_history(
ft4: crate::server::audio::snapshot_ft4_history(context, rig_filter),
ft2: crate::server::audio::snapshot_ft2_history(context, rig_filter),
wspr: crate::server::audio::snapshot_wspr_history(context, rig_filter),
wefax: crate::server::audio::snapshot_wefax_history(context, rig_filter),
}
}
@@ -400,10 +403,38 @@ pub async fn toggle_lrpt_decode(
.await
}
#[post("/toggle_wefax_decode")]
pub async fn toggle_wefax_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().decoders.wefax_decode_enabled;
send_command(
&rig_tx,
RigCommand::SetWefaxDecodeEnabled(!enabled),
query.into_inner().remote,
)
.await
}
// ============================================================================
// Decoder clear endpoints
// ============================================================================
#[post("/clear_wefax_decode")]
pub async fn clear_wefax_decode(
query: web::Query<RemoteQuery>,
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
) -> Result<HttpResponse, Error> {
send_command(
&rig_tx,
RigCommand::ResetWefaxDecoder,
query.into_inner().remote,
)
.await
}
#[post("/clear_lrpt_decode")]
pub async fn clear_lrpt_decode(
query: web::Query<RemoteQuery>,
@@ -595,6 +595,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(decoder::toggle_ft2_decode)
.service(decoder::toggle_wspr_decode)
.service(decoder::toggle_lrpt_decode)
.service(decoder::toggle_wefax_decode)
.service(decoder::clear_ais_decode)
.service(decoder::clear_vdes_decode)
.service(decoder::clear_aprs_decode)
@@ -605,6 +606,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(decoder::clear_ft2_decode)
.service(decoder::clear_wspr_decode)
.service(decoder::clear_lrpt_decode)
.service(decoder::clear_wefax_decode)
// Bookmark CRUD
.service(bookmarks::list_bookmarks)
.service(bookmarks::create_bookmark)
@@ -661,6 +663,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(assets::wspr_js)
.service(assets::cw_js)
.service(assets::sat_js)
.service(assets::wefax_js)
.service(assets::bookmarks_js)
.service(assets::scheduler_js)
.service(assets::sat_scheduler_js)
@@ -173,7 +173,7 @@ pub async fn set_vchan_mode(
fn bookmark_decoder_state(
bookmark: &crate::server::bookmarks::Bookmark,
) -> (bool, bool, bool, bool, bool, bool, 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;
@@ -181,6 +181,7 @@ fn bookmark_decoder_state(
let mut want_ft2 = false;
let mut want_wspr = false;
let mut want_lrpt = false;
let mut want_wefax = false;
for decoder in bookmark
.decoders
@@ -195,6 +196,7 @@ fn bookmark_decoder_state(
"ft2" => want_ft2 = true,
"wspr" => want_wspr = true,
"lrpt" => want_lrpt = true,
"wefax" => want_wefax = true,
_ => {}
}
}
@@ -207,6 +209,7 @@ fn bookmark_decoder_state(
want_ft2,
want_wspr,
want_lrpt,
want_wefax,
)
}
@@ -247,7 +250,7 @@ async fn apply_selected_channel(
let Some(bookmark) = bookmark_store_map.get_for_rig(remote, bookmark_id) else {
return Ok(());
};
let (want_aprs, want_hf_aprs, want_ft8, want_ft4, want_ft2, want_wspr, want_lrpt) =
let (want_aprs, want_hf_aprs, want_ft8, want_ft4, want_ft2, want_wspr, want_lrpt, want_wefax) =
bookmark_decoder_state(&bookmark);
let desired = [
RigCommand::SetAprsDecodeEnabled(want_aprs),
@@ -257,6 +260,7 @@ async fn apply_selected_channel(
RigCommand::SetFt2DecodeEnabled(want_ft2),
RigCommand::SetWsprDecodeEnabled(want_wspr),
RigCommand::SetLrptDecodeEnabled(want_lrpt),
RigCommand::SetWefaxDecodeEnabled(want_wefax),
];
for cmd in desired {
send_command_to_rig(rig_tx, remote, cmd).await?;
@@ -23,7 +23,8 @@ use tracing::warn;
use uuid::Uuid;
use trx_core::decode::{
AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, VdesMessage, WsprMessage,
AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, VdesMessage, WefaxMessage,
WsprMessage,
};
use trx_frontend::FrontendRuntimeContext;
@@ -296,6 +297,20 @@ fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
prune_wspr_history(context, &mut history);
}
fn record_wefax(context: &FrontendRuntimeContext, msg: WefaxMessage) {
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
let mut history = context
.decode_history
.wefax
.lock()
.expect("wefax history mutex poisoned");
history.push_back((Instant::now(), rig_id, msg));
// Wefax images are large; keep a small history.
while history.len() > 100 {
history.pop_front();
}
}
/// Returns `true` if the entry's rig_id matches the optional filter.
/// `None` filter means "all rigs".
fn matches_rig_filter(entry_rig: Option<&str>, filter: Option<&str>) -> bool {
@@ -471,6 +486,31 @@ pub fn snapshot_wspr_history(
.collect()
}
pub fn snapshot_wefax_history(
context: &FrontendRuntimeContext,
rig_filter: Option<&str>,
) -> Vec<WefaxMessage> {
let history = context
.decode_history
.wefax
.lock()
.expect("wefax history mutex poisoned");
history
.iter()
.filter(|(_, rid, _)| matches_rig_filter(rid.as_deref(), rig_filter))
.map(|(_, _, msg)| msg.clone())
.collect()
}
pub fn clear_wefax_history(context: &FrontendRuntimeContext) {
let mut history = context
.decode_history
.wefax
.lock()
.expect("wefax history mutex poisoned");
history.clear();
}
pub fn clear_aprs_history(context: &FrontendRuntimeContext) {
let mut history = context
.decode_history
@@ -584,6 +624,8 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
DecodedMessage::Ft4(msg) => record_ft4(&context, msg),
DecodedMessage::Ft2(msg) => record_ft2(&context, msg),
DecodedMessage::Wspr(msg) => record_wspr(&context, msg),
DecodedMessage::Wefax(msg) => record_wefax(&context, msg),
DecodedMessage::WefaxProgress(_) => {}
DecodedMessage::LrptImage(_) => {}
DecodedMessage::LrptProgress(_) => {}
},
@@ -1106,6 +1106,7 @@ async fn apply_scheduler_decoders(
let mut want_ft2 = false;
let mut want_wspr = false;
let mut want_lrpt = false;
let mut want_wefax = false;
let mut update_from = |bm: &crate::server::bookmarks::Bookmark| {
for decoder in bm
@@ -1121,6 +1122,7 @@ async fn apply_scheduler_decoders(
"ft2" => want_ft2 = true,
"wspr" => want_wspr = true,
"lrpt" => want_lrpt = true,
"wefax" => want_wefax = true,
_ => {}
}
}
@@ -1139,6 +1141,7 @@ async fn apply_scheduler_decoders(
("FT2", RigCommand::SetFt2DecodeEnabled(want_ft2)),
("WSPR", RigCommand::SetWsprDecodeEnabled(want_wspr)),
("LRPT", RigCommand::SetLrptDecodeEnabled(want_lrpt)),
("WEFAX", RigCommand::SetWefaxDecodeEnabled(want_wefax)),
];
for (label, cmd) in desired {
@@ -28,6 +28,7 @@ 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 SAT_JS: &str = include_str!("../assets/web/plugins/sat.js");
pub const WEFAX_JS: &str = include_str!("../assets/web/plugins/wefax.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 SAT_SCHEDULER_JS: &str = include_str!("../assets/web/plugins/sat-scheduler.js");