[feat](trx-protocol): add centralised decoder registry
Add DECODER_REGISTRY in trx-protocol::decoders as the single source of truth for all decoder metadata (activation mode, supported rig modes, background-decode capability). Replace duplicated resolver functions in background_decode.rs and sse.rs with shared resolve_bookmark_decoders(). Add GET /decoders endpoint to expose the registry to the frontend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3241,22 +3241,22 @@ function render(update) {
|
||||
const wsprStatus = document.getElementById("wspr-status");
|
||||
setModeBoundDecodeStatus(
|
||||
aisStatus,
|
||||
["AIS", "FM", "PKT"],
|
||||
"Select AIS or FM mode to decode",
|
||||
["AIS"],
|
||||
"Select AIS mode to decode",
|
||||
"Connected, listening for packets",
|
||||
);
|
||||
if (window.updateAisBar) window.updateAisBar();
|
||||
setModeBoundDecodeStatus(
|
||||
vdesStatus,
|
||||
["VDES", "FM"],
|
||||
"Select VDES or FM mode to decode",
|
||||
["VDES"],
|
||||
"Select VDES mode to decode",
|
||||
"Connected, listening for bursts",
|
||||
);
|
||||
if (window.updateVdesBar) window.updateVdesBar();
|
||||
setModeBoundDecodeStatus(
|
||||
aprsStatus,
|
||||
["PKT", "FM"],
|
||||
"Select FM mode to decode",
|
||||
["PKT"],
|
||||
"Select PKT mode to decode",
|
||||
"Connected, listening for packets",
|
||||
);
|
||||
if (window.updateAprsBar) window.updateAprsBar();
|
||||
@@ -8903,10 +8903,10 @@ function updateDecodeStatus(text) {
|
||||
const ft8 = document.getElementById("ft8-status");
|
||||
const ft4 = document.getElementById("ft4-status");
|
||||
const ft2 = document.getElementById("ft2-status");
|
||||
setModeBoundDecodeStatus(ais, ["AIS", "FM", "PKT"], "Select AIS or FM mode to decode", text);
|
||||
setModeBoundDecodeStatus(ais, ["AIS"], "Select AIS mode to decode", text);
|
||||
const vdesText = text === "Connected, listening for packets" ? "Connected, listening for bursts" : text;
|
||||
setModeBoundDecodeStatus(vdes, ["VDES", "FM"], "Select VDES or FM mode to decode", vdesText);
|
||||
setModeBoundDecodeStatus(aprs, ["PKT", "FM"], "Select FM mode to decode", text);
|
||||
setModeBoundDecodeStatus(vdes, ["VDES"], "Select VDES mode to decode", vdesText);
|
||||
setModeBoundDecodeStatus(aprs, ["PKT"], "Select PKT mode to decode", 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;
|
||||
|
||||
@@ -449,13 +449,14 @@
|
||||
<input type="number" id="bm-freq" class="status-input" required min="0" placeholder="e.g. 7074000" />
|
||||
</label>
|
||||
<label class="bm-label">Mode
|
||||
<input type="text" id="bm-mode" class="status-input" list="bm-mode-list" required placeholder="e.g. USB" />
|
||||
<input type="text" id="bm-mode" class="status-input" list="bm-mode-list" required placeholder="e.g. DIG" />
|
||||
<datalist id="bm-mode-list">
|
||||
<option value="LSB">
|
||||
<option value="USB">
|
||||
<option value="AM">
|
||||
<option value="SAM">
|
||||
<option value="FM">
|
||||
<option value="DIG">
|
||||
<option value="CW">
|
||||
<option value="WFM">
|
||||
</datalist>
|
||||
@@ -560,25 +561,25 @@
|
||||
<div class="plugin-item">
|
||||
<strong>FT8 Decoder</strong>
|
||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||
Decodes FT8 messages from RX audio (USB only, toggle required).
|
||||
Decodes FT8 messages from RX audio (DIG/USB only, toggle required).
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-item">
|
||||
<strong>FT4 Decoder</strong>
|
||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||
Decodes FT4 messages from RX audio (USB only, toggle required). 7.5-second slots.
|
||||
Decodes FT4 messages from RX audio (DIG/USB only, toggle required). 7.5-second slots.
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-item">
|
||||
<strong>FT2 Decoder</strong>
|
||||
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
|
||||
Decodes FT2 messages from RX audio (USB only, toggle required). 3.75-second slots.
|
||||
Decodes FT2 messages from RX audio (DIG/USB only, toggle required). 3.75-second slots.
|
||||
</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 (USB only, toggle required).
|
||||
Decodes WSPR messages from RX audio (DIG/USB only, toggle required).
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-item">
|
||||
|
||||
@@ -322,8 +322,7 @@ function renderAprsHistory() {
|
||||
|
||||
function updateAprsBar() {
|
||||
if (!aprsBarOverlay) return;
|
||||
const modeVal = (document.getElementById("mode")?.value || "").toUpperCase();
|
||||
const isPkt = modeVal === "PKT" || modeVal === "FM";
|
||||
const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
|
||||
const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
|
||||
const okFrames = aprsPacketHistory.filter((p) => p.crcOk && p._tsMs >= cutoffMs);
|
||||
const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@
|
||||
if (supported.length > 0) return supported;
|
||||
const mode = String(bookmark && bookmark.mode || "").trim().toUpperCase();
|
||||
if (mode === "AIS") return ["ais"];
|
||||
if (mode === "PKT" || mode === "FM") return ["aprs"];
|
||||
if (mode === "PKT") return ["aprs"];
|
||||
return supported;
|
||||
}
|
||||
|
||||
|
||||
@@ -428,9 +428,9 @@ async function bmApply(bm) {
|
||||
await postPath("/set_freq?hz=" + bm.freq_hz);
|
||||
}
|
||||
})();
|
||||
// Decoder toggles (USB / DIG / FM / PKT modes) — also fire-and-forget.
|
||||
// Decoder toggles (DIG / FM modes) — also fire-and-forget.
|
||||
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
|
||||
const decoderMode = bm.mode === "USB" || bm.mode === "DIG" || bm.mode === "FM" || bm.mode === "PKT";
|
||||
const decoderMode = bm.mode === "DIG" || bm.mode === "FM";
|
||||
const decoderPromise = (hasDecoders && decoderMode) ? (async () => {
|
||||
const statusResp = await fetch("/status");
|
||||
if (statusResp.ok) {
|
||||
|
||||
@@ -20,6 +20,15 @@ use trx_frontend::FrontendRuntimeContext;
|
||||
|
||||
use super::{gzip_bytes, send_command, RemoteQuery};
|
||||
|
||||
// ============================================================================
|
||||
// Decoder registry
|
||||
// ============================================================================
|
||||
|
||||
#[get("/decoders")]
|
||||
pub async fn decoder_registry() -> impl Responder {
|
||||
HttpResponse::Ok().json(trx_protocol::DECODER_REGISTRY)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Decode history types and helpers
|
||||
// ============================================================================
|
||||
|
||||
@@ -562,6 +562,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(sse::events)
|
||||
.service(sse::spectrum)
|
||||
// Decoder endpoints
|
||||
.service(decoder::decoder_registry)
|
||||
.service(decoder::decode_history)
|
||||
.service(decoder::decode_events)
|
||||
.service(decoder::toggle_aprs_decode)
|
||||
|
||||
@@ -135,30 +135,11 @@ fn sync_scheduler_vchannels(
|
||||
}
|
||||
|
||||
fn bookmark_decoder_kinds(bookmark: &crate::server::bookmarks::Bookmark) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for decoder in bookmark
|
||||
.decoders
|
||||
.iter()
|
||||
.map(|item| item.trim().to_ascii_lowercase())
|
||||
{
|
||||
if matches!(
|
||||
decoder.as_str(),
|
||||
"aprs" | "ais" | "ft8" | "ft4" | "ft2" | "wspr" | "hf-aprs"
|
||||
) && !out.iter().any(|existing| existing == &decoder)
|
||||
{
|
||||
out.push(decoder);
|
||||
}
|
||||
}
|
||||
|
||||
if !out.is_empty() {
|
||||
return out;
|
||||
}
|
||||
|
||||
match bookmark.mode.trim().to_ascii_uppercase().as_str() {
|
||||
"AIS" => vec!["ais".to_string()],
|
||||
"PKT" => vec!["aprs".to_string()],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
trx_protocol::decoders::resolve_bookmark_decoders(
|
||||
&bookmark.decoders,
|
||||
&bookmark.mode,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::server::bookmarks::{Bookmark, BookmarkStoreMap};
|
||||
use crate::server::scheduler::{SchedulerStatusMap, SharedSchedulerControlManager};
|
||||
use crate::server::vchan::{ClientChannel, ClientChannelManager};
|
||||
|
||||
const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "ft4", "ft2", "wspr", "hf-aprs"];
|
||||
use trx_protocol::decoders::resolve_bookmark_decoders;
|
||||
const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel";
|
||||
const VISIBLE_CHANNEL_KIND_NAME: &str = "VirtualChannel";
|
||||
|
||||
@@ -233,7 +233,7 @@ impl BackgroundDecodeManager {
|
||||
freq_hz: bookmark.map(|item| item.freq_hz),
|
||||
mode: bookmark.map(|item| item.mode.clone()),
|
||||
decoder_kinds: bookmark
|
||||
.map(bookmark_supported_decoder_kinds)
|
||||
.map(bookmark_decoder_kinds)
|
||||
.unwrap_or_default(),
|
||||
state: "inactive".to_string(),
|
||||
channel_kind: None,
|
||||
@@ -383,7 +383,7 @@ impl BackgroundDecodeManager {
|
||||
continue;
|
||||
};
|
||||
|
||||
let decoder_kinds = bookmark_supported_decoder_kinds(bookmark);
|
||||
let decoder_kinds = bookmark_decoder_kinds(bookmark);
|
||||
let mut status = BackgroundDecodeBookmarkStatus {
|
||||
bookmark_id: bookmark.id.clone(),
|
||||
bookmark_name: Some(bookmark.name.clone()),
|
||||
@@ -609,30 +609,8 @@ fn dedup_ids(ids: &[String]) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
fn supported_decoder_kinds(decoders: &[String]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for decoder in decoders {
|
||||
let decoder = decoder.trim().to_ascii_lowercase();
|
||||
if SUPPORTED_DECODER_KINDS.contains(&decoder.as_str())
|
||||
&& !out.iter().any(|existing| existing == &decoder)
|
||||
{
|
||||
out.push(decoder);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn bookmark_supported_decoder_kinds(bookmark: &Bookmark) -> Vec<String> {
|
||||
let explicit = supported_decoder_kinds(&bookmark.decoders);
|
||||
if !explicit.is_empty() {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
match bookmark.mode.trim().to_ascii_uppercase().as_str() {
|
||||
"AIS" => vec!["ais".to_string()],
|
||||
"PKT" | "FM" => vec!["aprs".to_string()],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
fn bookmark_decoder_kinds(bookmark: &Bookmark) -> Vec<String> {
|
||||
resolve_bookmark_decoders(&bookmark.decoders, &bookmark.mode, true)
|
||||
}
|
||||
|
||||
fn channel_matches_bookmark(channel: &ClientChannel, bookmark: &Bookmark) -> bool {
|
||||
|
||||
@@ -966,8 +966,7 @@ async fn apply_scheduler_decoders(
|
||||
bookmark: &crate::server::bookmarks::Bookmark,
|
||||
extra_bookmarks: &[crate::server::bookmarks::Bookmark],
|
||||
) {
|
||||
let mut want_aprs = bookmark.mode.trim().eq_ignore_ascii_case("PKT")
|
||||
|| bookmark.mode.trim().eq_ignore_ascii_case("FM");
|
||||
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;
|
||||
|
||||
@@ -723,7 +723,7 @@ mod tests {
|
||||
|
||||
mgr.init_rig(rig_id, 14_074_000, "USB");
|
||||
let channel = mgr
|
||||
.allocate(session_id, rig_id, 14_075_000, "USB")
|
||||
.allocate(session_id, rig_id, 14_075_000, "DIG")
|
||||
.expect("allocate vchan");
|
||||
|
||||
assert_eq!(mgr.channels(rig_id).len(), 2);
|
||||
@@ -747,7 +747,7 @@ mod tests {
|
||||
&[(
|
||||
"bm-ft8".to_string(),
|
||||
14_074_000,
|
||||
"USB".to_string(),
|
||||
"DIG".to_string(),
|
||||
3_000,
|
||||
vec!["ft8".to_string()],
|
||||
)],
|
||||
@@ -756,7 +756,7 @@ mod tests {
|
||||
let channels = mgr.channels(rig_id);
|
||||
assert_eq!(channels.len(), 2);
|
||||
assert_eq!(channels[1].freq_hz, 14_074_000);
|
||||
assert_eq!(channels[1].mode, "USB");
|
||||
assert_eq!(channels[1].mode, "DIG");
|
||||
assert_eq!(channels[1].bandwidth_hz, 3_000);
|
||||
assert_eq!(channels[1].subscribers, 0);
|
||||
assert!(channels[1].permanent);
|
||||
@@ -770,14 +770,14 @@ mod tests {
|
||||
|
||||
mgr.init_rig(rig_id, 14_074_000, "USB");
|
||||
let _channel = mgr
|
||||
.allocate(session_id, rig_id, 14_075_000, "USB")
|
||||
.allocate(session_id, rig_id, 14_075_000, "DIG")
|
||||
.expect("allocate vchan");
|
||||
mgr.sync_scheduler_channels(
|
||||
rig_id,
|
||||
&[(
|
||||
"bm-ft8".to_string(),
|
||||
14_074_000,
|
||||
"USB".to_string(),
|
||||
"DIG".to_string(),
|
||||
3_000,
|
||||
vec!["ft8".to_string()],
|
||||
)],
|
||||
@@ -787,7 +787,7 @@ mod tests {
|
||||
|
||||
let channels = mgr.channels(rig_id);
|
||||
assert_eq!(channels.len(), 2);
|
||||
assert_eq!(channels[1].mode, "USB");
|
||||
assert_eq!(channels[1].mode, "DIG");
|
||||
assert_eq!(channels[1].subscribers, 0);
|
||||
}
|
||||
|
||||
@@ -803,7 +803,7 @@ mod tests {
|
||||
&[(
|
||||
"bm-aprs".to_string(),
|
||||
144_800_000,
|
||||
"FM".to_string(),
|
||||
"PKT".to_string(),
|
||||
12_500,
|
||||
vec!["aprs".to_string()],
|
||||
)],
|
||||
|
||||
Reference in New Issue
Block a user