[fix](trx-frontend-http): honor scheduler explicit center freq

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-13 15:13:04 +01:00
parent e4487d9037
commit 968dc84997
@@ -302,11 +302,11 @@ fn entry_is_active(entry: &ScheduleEntry, now_min: f64) -> bool {
}
}
fn timespan_bookmark_id(
fn timespan_active_entry(
entries: &[ScheduleEntry],
now_min: f64,
default_interleave: Option<u32>,
) -> Option<String> {
) -> Option<&ScheduleEntry> {
let active: Vec<&ScheduleEntry> = entries
.iter()
.filter(|e| entry_is_active(e, now_min))
@@ -331,14 +331,14 @@ fn timespan_bookmark_id(
for (entry, &dur) in active.iter().zip(durations.iter()) {
cum += dur as u64;
if pos < cum {
return Some(entry.bookmark_id.clone());
return Some(*entry);
}
}
}
}
// Default: first matching entry wins.
Some(active[0].bookmark_id.clone())
Some(active[0])
}
/// Current UTC time as minutes since midnight.
@@ -372,6 +372,13 @@ pub struct SchedulerStatus {
/// Shared mutable state for scheduler status (one entry per rig).
pub type SchedulerStatusMap = Arc<RwLock<HashMap<String, SchedulerStatus>>>;
#[derive(Debug, Clone, PartialEq, Eq)]
struct AppliedTarget {
bookmark_id: String,
center_hz: Option<u64>,
extra_bookmark_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SchedulerControlSummary {
pub connected_sessions: usize,
@@ -446,8 +453,9 @@ pub fn spawn_scheduler_task(
) {
tokio::spawn(async move {
let mut interval = time::interval(Duration::from_secs(30));
// Track last applied bookmark per rig to avoid redundant retunes.
let mut last_applied: HashMap<String, String> = HashMap::new();
// Track the full last applied target per rig to avoid redundant retunes
// while still honoring center-frequency or extra-channel changes.
let mut last_applied: HashMap<String, AppliedTarget> = HashMap::new();
let mut last_control_allowed = false;
loop {
@@ -471,21 +479,40 @@ pub fn spawn_scheduler_task(
continue;
}
let target_bm_id = match &config.mode {
let (bm_id, center_hz, extra_bm_ids) = match &config.mode {
SchedulerMode::Disabled => continue,
SchedulerMode::Grayline => config
.grayline
.as_ref()
.and_then(|gl| grayline_bookmark_id(gl, now_min)),
SchedulerMode::Grayline => {
let Some(bm_id) = config
.grayline
.as_ref()
.and_then(|gl| grayline_bookmark_id(gl, now_min))
else {
continue;
};
(bm_id, None, Vec::new())
}
SchedulerMode::TimeSpan => {
timespan_bookmark_id(&config.entries, now_min, config.interleave_min)
let Some(entry) =
timespan_active_entry(&config.entries, now_min, config.interleave_min)
else {
continue;
};
(
entry.bookmark_id.clone(),
entry.center_hz,
entry.bookmark_ids.clone(),
)
}
};
let Some(bm_id) = target_bm_id else { continue };
let target = AppliedTarget {
bookmark_id: bm_id.clone(),
center_hz,
extra_bookmark_ids: extra_bm_ids.clone(),
};
// Already at this bookmark — skip.
if last_applied.get(&config.rig_id) == Some(&bm_id) {
// Already at this exact scheduled target — skip.
if last_applied.get(&config.rig_id) == Some(&target) {
continue;
}
@@ -497,12 +524,6 @@ pub fn spawn_scheduler_task(
continue;
};
// Resolve the matching entry to pick up center_hz / bookmark_ids.
let active_entry = config.entries.iter().find(|e| e.bookmark_id == bm_id);
let center_hz = active_entry.and_then(|e| e.center_hz);
let extra_bm_ids: Vec<String> = active_entry
.map(|e| e.bookmark_ids.clone())
.unwrap_or_default();
let extra_bookmarks: Vec<_> = extra_bm_ids
.iter()
.filter_map(|id| bookmarks.get(id))
@@ -566,7 +587,7 @@ pub fn spawn_scheduler_task(
)
.await;
last_applied.insert(config.rig_id.clone(), bm_id.clone());
last_applied.insert(config.rig_id.clone(), target);
// Update status map (includes center_hz + extra bookmark_ids
// so the JS frontend can set up virtual channels on connect).
@@ -832,3 +853,52 @@ pub async fn put_scheduler_control(
}
HttpResponse::Ok().json(summary)
}
#[cfg(test)]
mod tests {
use super::{timespan_active_entry, ScheduleEntry};
fn entry(
id: &str,
start_min: u32,
end_min: u32,
bookmark_id: &str,
center_hz: Option<u64>,
interleave_min: Option<u32>,
) -> ScheduleEntry {
ScheduleEntry {
id: id.to_string(),
start_min,
end_min,
bookmark_id: bookmark_id.to_string(),
label: None,
interleave_min,
center_hz,
bookmark_ids: Vec::new(),
}
}
#[test]
fn timespan_active_entry_returns_selected_overlap_entry() {
let entries = vec![
entry("slot-a", 0, 0, "bm-shared", Some(144_500_000), Some(10)),
entry("slot-b", 0, 0, "bm-shared", Some(144_300_000), Some(10)),
];
let active = timespan_active_entry(&entries, 15.0, None).expect("active entry");
assert_eq!(active.id, "slot-b");
assert_eq!(active.center_hz, Some(144_300_000));
}
#[test]
fn timespan_active_entry_returns_first_match_without_interleave() {
let entries = vec![
entry("slot-a", 60, 120, "bm-a", Some(14_100_000), None),
entry("slot-b", 60, 120, "bm-b", Some(14_200_000), None),
];
let active = timespan_active_entry(&entries, 90.0, None).expect("active entry");
assert_eq!(active.id, "slot-a");
assert_eq!(active.center_hz, Some(14_100_000));
}
}