From f6282d17cab8ea1d123751f41ad57042151f021a Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 29 Mar 2026 23:49:09 +0200 Subject: [PATCH] [fix](trx-client): remove dead NOAA APT decoder, fix LRPT bookmark activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the wxsat/NOAA APT checkbox from bookmark decoder form and all JS references — the APT decoder no longer exists. Fix LRPT decoder not activating when an FM-mode bookmark is applied: bmApply() gated decoder toggles on DIG mode only, so LRPT bookmarks (which use FM) never triggered SetLrptDecodeEnabled. Gate on DIG or FM. Wire satellite pass scheduling into the scheduler loop: check configured satellite entries against live pass predictions, activate the satellite's bookmark (enabling LRPT decoder) when a pass is active, and expose active_satellite in SchedulerStatus for the frontend. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/index.html | 1 - .../assets/web/plugins/bookmarks.js | 10 +- .../trx-frontend-http/src/scheduler.rs | 182 +++++++++++++++++- 3 files changed, 186 insertions(+), 7 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 03e863e..8795976 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -472,7 +472,6 @@ - diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js index fb836ff..3eabfb4 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/bookmarks.js @@ -236,7 +236,6 @@ function bmReadDecoders() { if (document.getElementById("bm-dec-ft2").checked) decoders.push("ft2"); 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-wxsat").checked) decoders.push("wxsat"); if (document.getElementById("bm-dec-lrpt").checked) decoders.push("lrpt"); return decoders; } @@ -251,7 +250,6 @@ function bmWriteDecoders(decoders) { document.getElementById("bm-dec-ft2").checked = list.includes("ft2"); document.getElementById("bm-dec-wspr").checked = list.includes("wspr"); document.getElementById("bm-dec-hf-aprs").checked = list.includes("hf-aprs"); - document.getElementById("bm-dec-wxsat").checked = list.includes("wxsat"); document.getElementById("bm-dec-lrpt").checked = list.includes("lrpt"); } @@ -430,8 +428,10 @@ async function bmApply(bm) { await postPath("/set_freq?hz=" + bm.freq_hz); } })(); - // Decoder toggles (DIG mode) — also fire-and-forget. - const decoderPromise = (bm.mode === "DIG" && Array.isArray(bm.decoders)) ? (async () => { + // Decoder toggles (DIG / FM modes) — also fire-and-forget. + const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0; + const decoderMode = bm.mode === "DIG" || bm.mode === "FM"; + const decoderPromise = (hasDecoders && decoderMode) ? (async () => { const statusResp = await fetch("/status"); if (statusResp.ok) { const st = await statusResp.json(); @@ -441,7 +441,7 @@ async function bmApply(bm) { toggles.push(postPath("/toggle_" + key.replace(/-/g, "_") + "_decode")); } }; - check("ft8"); check("ft4"); check("ft2"); check("wspr"); check("hf-aprs"); check("wxsat"); check("lrpt"); + check("ft8"); check("ft4"); check("ft2"); check("wspr"); check("hf-aprs"); check("lrpt"); if (toggles.length) await Promise.all(toggles); } })() : Promise.resolve(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs index e7d1357..995992b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs @@ -520,6 +520,9 @@ pub struct SchedulerStatus { /// Additional bookmark IDs active alongside the primary (virtual channels). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub last_bookmark_ids: Vec, + /// Name of the satellite whose pass is currently active (if any). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub active_satellite: Option, } #[allow(clippy::too_many_arguments)] @@ -532,6 +535,7 @@ async fn apply_scheduler_target( bookmark_id: &str, center_hz: Option, extra_bm_ids: &[String], + satellite_name: Option<&str>, ) -> Result { let bookmark = bookmarks .get_for_rig(remote, bookmark_id) @@ -595,6 +599,7 @@ async fn apply_scheduler_target( ), last_center_hz: center_hz, last_bookmark_ids: extra_bm_ids.to_vec(), + active_satellite: satellite_name.map(str::to_string), }; { @@ -613,6 +618,7 @@ struct AppliedTarget { bookmark_id: String, center_hz: Option, extra_bookmark_ids: Vec, + satellite: Option, } #[derive(Debug, Clone, Serialize, Default)] @@ -680,7 +686,7 @@ impl SchedulerControlManager { pub type SharedSchedulerControlManager = Arc; pub fn spawn_scheduler_task( - _context: Arc, + context: Arc, rig_tx: mpsc::Sender, store: Arc, bookmarks: Arc, @@ -709,8 +715,91 @@ pub fn spawn_scheduler_task( let configs = store.list_all(); let now_min = utc_minutes_now(); + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; for config in configs { + // ── Satellite pass scheduling ────────────────────────── + // Satellite passes take priority over the base scheduler + // mode. When a configured satellite has an active pass + // above its minimum elevation, we retune to the + // satellite's bookmark and enable its decoders (e.g. + // LRPT). + if let Some(sat_target) = find_active_satellite_target( + &config, + &context, + now_ms, + ) { + let target = AppliedTarget { + bookmark_id: sat_target.bookmark_id.clone(), + center_hz: sat_target.center_hz, + extra_bookmark_ids: sat_target.extra_bm_ids.clone(), + satellite: Some(sat_target.satellite.clone()), + }; + + if last_applied.get(&config.remote) == Some(&target) { + continue; + } + + let Some(bm) = + bookmarks.get_for_rig(&config.remote, &sat_target.bookmark_id) + else { + warn!( + "scheduler: satellite bookmark '{}' not found for remote '{}'", + sat_target.bookmark_id, config.remote + ); + continue; + }; + + info!( + "scheduler: remote '{}' → satellite '{}' → bookmark '{}' ({} Hz {})", + config.remote, sat_target.satellite, bm.name, bm.freq_hz, bm.mode + ); + + if let Err(e) = apply_scheduler_target( + &rig_tx, + &config.remote, + &status_map, + &bookmarks, + Some(&sat_target.entry_id), + &sat_target.bookmark_id, + sat_target.center_hz, + &sat_target.extra_bm_ids, + Some(&sat_target.satellite), + ) + .await + { + warn!( + "scheduler: failed to apply satellite target for '{}': {e}", + config.remote + ); + continue; + } + + last_applied.insert(config.remote.clone(), target); + continue; + } + + // If the previous target was a satellite pass that has + // ended, clear it so the base mode can resume. + if last_applied + .get(&config.remote) + .is_some_and(|t| t.satellite.is_some()) + { + last_applied.remove(&config.remote); + // Clear the active_satellite from status. + if let Ok(mut map) = + status_map.write() + { + if let Some(st) = map.get_mut(&config.remote) { + st.active_satellite = None; + } + } + } + + // ── Base scheduler mode ─────────────────────────────── if config.mode == SchedulerMode::Disabled { continue; } @@ -746,6 +835,7 @@ pub fn spawn_scheduler_task( bookmark_id: bm_id.clone(), center_hz, extra_bookmark_ids: extra_bm_ids.clone(), + satellite: None, }; // Already at this exact scheduled target — skip. @@ -775,6 +865,7 @@ pub fn spawn_scheduler_task( &bm_id, center_hz, &extra_bm_ids, + None, ) .await { @@ -791,6 +882,93 @@ pub fn spawn_scheduler_task( }); } +// ============================================================================ +// Satellite pass helpers +// ============================================================================ + +struct SatelliteTarget { + entry_id: String, + satellite: String, + bookmark_id: String, + center_hz: Option, + extra_bm_ids: Vec, +} + +/// Check if any configured satellite has an active pass right now. +/// +/// Returns the highest-priority (lowest `priority` value) satellite entry +/// whose NORAD ID has a pass in progress with max elevation above the +/// entry's configured minimum. +fn find_active_satellite_target( + config: &SchedulerConfig, + context: &FrontendRuntimeContext, + now_ms: i64, +) -> Option { + let sat_cfg = config.satellites.as_ref().filter(|s| s.enabled)?; + if sat_cfg.entries.is_empty() { + return None; + } + + let passes = context + .routing + .sat_passes + .read() + .ok() + .and_then(|g| g.clone())?; + + // Build a lookup: NORAD ID → active pass (AOS ≤ now ≤ LOS). + let active_passes: HashMap = passes + .passes + .iter() + .filter(|p| now_ms >= p.aos_ms && now_ms <= p.los_ms) + .map(|p| (p.norad_id, p)) + .collect(); + + if active_passes.is_empty() { + return None; + } + + // Among configured satellites with an active pass that meets the + // minimum elevation requirement, pick the one with the best (lowest) + // priority. Pre-tune window: accept passes that are about to start + // within `pretune_secs`. + let pretune_ms = (sat_cfg.pretune_secs as i64) * 1000; + + let mut best: Option<(&SatelliteEntry, &trx_core::geo::PassPrediction)> = None; + + for entry in &sat_cfg.entries { + // Check for active pass or imminent pass within pretune window. + let pass = active_passes.get(&entry.norad_id).copied().or_else(|| { + passes.passes.iter().find(|p| { + p.norad_id == entry.norad_id + && p.aos_ms > now_ms + && p.aos_ms <= now_ms + pretune_ms + }) + }); + + let Some(pass) = pass else { continue }; + + if pass.max_elevation_deg < entry.min_elevation_deg { + continue; + } + + match &best { + Some((prev_entry, _)) if entry.priority >= prev_entry.priority => {} + _ => best = Some((entry, pass)), + } + } + + let (entry, _pass) = best?; + + Some(SatelliteTarget { + entry_id: entry.id.clone(), + satellite: entry.satellite.clone(), + bookmark_id: entry.bookmark_id.clone(), + center_hz: entry.center_hz, + extra_bm_ids: entry.bookmark_ids.clone(), + }) +} + async fn apply_scheduler_decoders( rig_tx: &mpsc::Sender, remote: &str, @@ -885,6 +1063,7 @@ async fn apply_last_scheduler_cycle( &bookmark_id, status.last_center_hz, &status.last_bookmark_ids, + status.active_satellite.as_deref(), ) .await { @@ -1017,6 +1196,7 @@ pub async fn put_scheduler_activate_entry( &entry.bookmark_id, entry.center_hz, &entry.bookmark_ids, + None, ) .await {