[feat](trx-frontend-http): expand background decode selection

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-12 22:57:25 +01:00
parent 34d0ec8ca8
commit 95fccd3da6
6 changed files with 40 additions and 5 deletions
@@ -358,6 +358,8 @@
</label> </label>
<div class="bm-label">Decoders <div class="bm-label">Decoders
<div class="bm-decoder-checks"> <div class="bm-decoder-checks">
<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-ft8" value="ft8" /> FT8</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-wspr" value="wspr" /> WSPR</label>
<label class="bm-decoder-check"><input type="checkbox" id="bm-dec-hf-aprs" value="hf-aprs" /> HF APRS</label> <label class="bm-decoder-check"><input type="checkbox" id="bm-dec-hf-aprs" value="hf-aprs" /> HF APRS</label>
@@ -5,7 +5,7 @@
(function () { (function () {
"use strict"; "use strict";
const SUPPORTED_DECODERS = ["ft8", "wspr", "hf-aprs"]; const SUPPORTED_DECODERS = ["aprs", "ais", "ft8", "wspr", "hf-aprs"];
let backgroundDecodeRole = null; let backgroundDecodeRole = null;
let currentRigId = null; let currentRigId = null;
@@ -303,6 +303,7 @@
case "no_supported_decoders": return "Unsupported"; case "no_supported_decoders": return "Unsupported";
case "disabled": return "Disabled"; case "disabled": return "Disabled";
case "handled_by_scheduler": return "Scheduler"; case "handled_by_scheduler": return "Scheduler";
case "handled_by_virtual_channel": return "VChan";
default: return "Inactive"; default: return "Inactive";
} }
} }
@@ -148,6 +148,8 @@ function bmRender(list) {
// Read decoder checkboxes and return an array of selected decoder names. // Read decoder checkboxes and return an array of selected decoder names.
function bmReadDecoders() { function bmReadDecoders() {
const decoders = []; const decoders = [];
if (document.getElementById("bm-dec-aprs").checked) decoders.push("aprs");
if (document.getElementById("bm-dec-ais").checked) decoders.push("ais");
if (document.getElementById("bm-dec-ft8").checked) decoders.push("ft8"); if (document.getElementById("bm-dec-ft8").checked) decoders.push("ft8");
if (document.getElementById("bm-dec-wspr").checked) decoders.push("wspr"); if (document.getElementById("bm-dec-wspr").checked) decoders.push("wspr");
if (document.getElementById("bm-dec-hf-aprs").checked) decoders.push("hf-aprs"); if (document.getElementById("bm-dec-hf-aprs").checked) decoders.push("hf-aprs");
@@ -157,6 +159,8 @@ function bmReadDecoders() {
// Set decoder checkboxes to match the given array. // Set decoder checkboxes to match the given array.
function bmWriteDecoders(decoders) { function bmWriteDecoders(decoders) {
const list = decoders || []; const list = decoders || [];
document.getElementById("bm-dec-aprs").checked = list.includes("aprs");
document.getElementById("bm-dec-ais").checked = list.includes("ais");
document.getElementById("bm-dec-ft8").checked = list.includes("ft8"); document.getElementById("bm-dec-ft8").checked = list.includes("ft8");
document.getElementById("bm-dec-wspr").checked = list.includes("wspr"); document.getElementById("bm-dec-wspr").checked = list.includes("wspr");
document.getElementById("bm-dec-hf-aprs").checked = list.includes("hf-aprs"); document.getElementById("bm-dec-hf-aprs").checked = list.includes("hf-aprs");
@@ -3559,7 +3559,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
.bgd-status-state[data-state="waiting_for_spectrum"], .bgd-status-state[data-state="waiting_for_spectrum"],
.bgd-status-state[data-state="waiting_for_user"], .bgd-status-state[data-state="waiting_for_user"],
.bgd-status-state[data-state="inactive"], .bgd-status-state[data-state="inactive"],
.bgd-status-state[data-state="handled_by_scheduler"] { .bgd-status-state[data-state="handled_by_scheduler"],
.bgd-status-state[data-state="handled_by_virtual_channel"] {
color: var(--accent-yellow); color: var(--accent-yellow);
} }
.bgd-status-state[data-state="missing_bookmark"], .bgd-status-state[data-state="missing_bookmark"],
@@ -19,9 +19,11 @@ use uuid::Uuid;
use crate::server::bookmarks::{Bookmark, BookmarkStore}; use crate::server::bookmarks::{Bookmark, BookmarkStore};
use crate::server::scheduler::SchedulerStatusMap; use crate::server::scheduler::SchedulerStatusMap;
use crate::server::vchan::{ClientChannel, ClientChannelManager};
const SUPPORTED_DECODER_KINDS: &[&str] = &["ft8", "wspr", "hf-aprs"]; const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "wspr", "hf-aprs"];
const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel"; const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel";
const VISIBLE_CHANNEL_KIND_NAME: &str = "VirtualChannel";
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BackgroundDecodeConfig { pub struct BackgroundDecodeConfig {
@@ -122,6 +124,7 @@ pub struct BackgroundDecodeManager {
bookmarks: Arc<BookmarkStore>, bookmarks: Arc<BookmarkStore>,
context: Arc<FrontendRuntimeContext>, context: Arc<FrontendRuntimeContext>,
scheduler_status: SchedulerStatusMap, scheduler_status: SchedulerStatusMap,
vchan_mgr: Arc<ClientChannelManager>,
status: Arc<RwLock<HashMap<String, BackgroundDecodeStatus>>>, status: Arc<RwLock<HashMap<String, BackgroundDecodeStatus>>>,
notify_tx: broadcast::Sender<()>, notify_tx: broadcast::Sender<()>,
} }
@@ -132,6 +135,7 @@ impl BackgroundDecodeManager {
bookmarks: Arc<BookmarkStore>, bookmarks: Arc<BookmarkStore>,
context: Arc<FrontendRuntimeContext>, context: Arc<FrontendRuntimeContext>,
scheduler_status: SchedulerStatusMap, scheduler_status: SchedulerStatusMap,
vchan_mgr: Arc<ClientChannelManager>,
) -> Arc<Self> { ) -> Arc<Self> {
let (notify_tx, _) = broadcast::channel(16); let (notify_tx, _) = broadcast::channel(16);
Arc::new(Self { Arc::new(Self {
@@ -139,6 +143,7 @@ impl BackgroundDecodeManager {
bookmarks, bookmarks,
context, context,
scheduler_status, scheduler_status,
vchan_mgr,
status: Arc::new(RwLock::new(HashMap::new())), status: Arc::new(RwLock::new(HashMap::new())),
notify_tx, notify_tx,
}) })
@@ -280,6 +285,13 @@ impl BackgroundDecodeManager {
&& channel.decoder_kinds == desired.decoder_kinds && channel.decoder_kinds == desired.decoder_kinds
} }
fn virtual_channels_cover_bookmark(&self, rig_id: &str, bookmark: &Bookmark) -> bool {
self.vchan_mgr
.channels(rig_id)
.into_iter()
.any(|channel| channel_matches_bookmark(&channel, bookmark))
}
fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) { fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) {
let active_rig_id = self.active_rig_id(); let active_rig_id = self.active_rig_id();
@@ -367,6 +379,13 @@ impl BackgroundDecodeManager {
continue; continue;
} }
if self.virtual_channels_cover_bookmark(&rig_id, bookmark) {
status.state = "handled_by_virtual_channel".to_string();
status.channel_kind = Some(VISIBLE_CHANNEL_KIND_NAME.to_string());
statuses.push(status);
continue;
}
let (Some(center_hz), Some(half_span_hz)) = (center_hz, half_span_hz) else { let (Some(center_hz), Some(half_span_hz)) = (center_hz, half_span_hz) else {
status.state = "waiting_for_spectrum".to_string(); status.state = "waiting_for_spectrum".to_string();
statuses.push(status); statuses.push(status);
@@ -506,6 +525,14 @@ fn supported_decoder_kinds(decoders: &[String]) -> Vec<String> {
out out
} }
fn channel_matches_bookmark(channel: &ClientChannel, bookmark: &Bookmark) -> bool {
channel.freq_hz == bookmark.freq_hz && normalized_mode(&channel.mode) == normalized_mode(&bookmark.mode)
}
fn normalized_mode(mode: &str) -> String {
mode.trim().to_ascii_lowercase()
}
#[get("/background-decode/{rig_id}")] #[get("/background-decode/{rig_id}")]
pub async fn get_background_decode( pub async fn get_background_decode(
path: web::Path<String>, path: web::Path<String>,
@@ -88,16 +88,16 @@ async fn serve(
let background_decode_path = BackgroundDecodeStore::default_path(); let background_decode_path = BackgroundDecodeStore::default_path();
let background_decode_store = let background_decode_store =
Arc::new(BackgroundDecodeStore::open(&background_decode_path)); Arc::new(BackgroundDecodeStore::open(&background_decode_path));
let vchan_mgr = Arc::new(ClientChannelManager::new(4));
let background_decode_mgr = BackgroundDecodeManager::new( let background_decode_mgr = BackgroundDecodeManager::new(
background_decode_store, background_decode_store,
bookmark_store.clone(), bookmark_store.clone(),
context.clone(), context.clone(),
scheduler_status.clone(), scheduler_status.clone(),
vchan_mgr.clone(),
); );
background_decode_mgr.spawn(); background_decode_mgr.spawn();
let vchan_mgr = Arc::new(ClientChannelManager::new(4));
// Wire the audio-command sender so allocate/delete/freq/mode operations on // Wire the audio-command sender so allocate/delete/freq/mode operations on
// virtual channels are forwarded to the audio-client task. // virtual channels are forwarded to the audio-client task.
if let Ok(guard) = context.vchan_audio_cmd.lock() { if let Ok(guard) = context.vchan_audio_cmd.lock() {