[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:
@@ -302,11 +302,11 @@ fn entry_is_active(entry: &ScheduleEntry, now_min: f64) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timespan_bookmark_id(
|
fn timespan_active_entry(
|
||||||
entries: &[ScheduleEntry],
|
entries: &[ScheduleEntry],
|
||||||
now_min: f64,
|
now_min: f64,
|
||||||
default_interleave: Option<u32>,
|
default_interleave: Option<u32>,
|
||||||
) -> Option<String> {
|
) -> Option<&ScheduleEntry> {
|
||||||
let active: Vec<&ScheduleEntry> = entries
|
let active: Vec<&ScheduleEntry> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|e| entry_is_active(e, now_min))
|
.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()) {
|
for (entry, &dur) in active.iter().zip(durations.iter()) {
|
||||||
cum += dur as u64;
|
cum += dur as u64;
|
||||||
if pos < cum {
|
if pos < cum {
|
||||||
return Some(entry.bookmark_id.clone());
|
return Some(*entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: first matching entry wins.
|
// Default: first matching entry wins.
|
||||||
Some(active[0].bookmark_id.clone())
|
Some(active[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current UTC time as minutes since midnight.
|
/// Current UTC time as minutes since midnight.
|
||||||
@@ -372,6 +372,13 @@ pub struct SchedulerStatus {
|
|||||||
/// Shared mutable state for scheduler status (one entry per rig).
|
/// Shared mutable state for scheduler status (one entry per rig).
|
||||||
pub type SchedulerStatusMap = Arc<RwLock<HashMap<String, SchedulerStatus>>>;
|
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)]
|
#[derive(Debug, Clone, Serialize, Default)]
|
||||||
pub struct SchedulerControlSummary {
|
pub struct SchedulerControlSummary {
|
||||||
pub connected_sessions: usize,
|
pub connected_sessions: usize,
|
||||||
@@ -446,8 +453,9 @@ pub fn spawn_scheduler_task(
|
|||||||
) {
|
) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut interval = time::interval(Duration::from_secs(30));
|
let mut interval = time::interval(Duration::from_secs(30));
|
||||||
// Track last applied bookmark per rig to avoid redundant retunes.
|
// Track the full last applied target per rig to avoid redundant retunes
|
||||||
let mut last_applied: HashMap<String, String> = HashMap::new();
|
// while still honoring center-frequency or extra-channel changes.
|
||||||
|
let mut last_applied: HashMap<String, AppliedTarget> = HashMap::new();
|
||||||
let mut last_control_allowed = false;
|
let mut last_control_allowed = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -471,21 +479,40 @@ pub fn spawn_scheduler_task(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_bm_id = match &config.mode {
|
let (bm_id, center_hz, extra_bm_ids) = match &config.mode {
|
||||||
SchedulerMode::Disabled => continue,
|
SchedulerMode::Disabled => continue,
|
||||||
SchedulerMode::Grayline => config
|
SchedulerMode::Grayline => {
|
||||||
|
let Some(bm_id) = config
|
||||||
.grayline
|
.grayline
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|gl| grayline_bookmark_id(gl, now_min)),
|
.and_then(|gl| grayline_bookmark_id(gl, now_min))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
(bm_id, None, Vec::new())
|
||||||
|
}
|
||||||
SchedulerMode::TimeSpan => {
|
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.
|
// Already at this exact scheduled target — skip.
|
||||||
if last_applied.get(&config.rig_id) == Some(&bm_id) {
|
if last_applied.get(&config.rig_id) == Some(&target) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,12 +524,6 @@ pub fn spawn_scheduler_task(
|
|||||||
continue;
|
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
|
let extra_bookmarks: Vec<_> = extra_bm_ids
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|id| bookmarks.get(id))
|
.filter_map(|id| bookmarks.get(id))
|
||||||
@@ -566,7 +587,7 @@ pub fn spawn_scheduler_task(
|
|||||||
)
|
)
|
||||||
.await;
|
.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
|
// Update status map (includes center_hz + extra bookmark_ids
|
||||||
// so the JS frontend can set up virtual channels on connect).
|
// 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)
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user