[fix](trx-frontend-http): show scheduler channels on connect
Materialize scheduler-managed virtual channels before the\ninitial channels SSE event when the scheduler currently\ncontrols the rig.\n\nVerification: cargo test -p trx-frontend-http vchan\n\nCo-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -223,6 +223,8 @@ pub async fn events(
|
|||||||
clients: web::Data<Arc<AtomicUsize>>,
|
clients: web::Data<Arc<AtomicUsize>>,
|
||||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||||
vchan_mgr: web::Data<Arc<ClientChannelManager>>,
|
vchan_mgr: web::Data<Arc<ClientChannelManager>>,
|
||||||
|
bookmark_store: web::Data<Arc<crate::server::bookmarks::BookmarkStore>>,
|
||||||
|
scheduler_status: web::Data<crate::server::scheduler::SchedulerStatusMap>,
|
||||||
scheduler_control: web::Data<crate::server::scheduler::SharedSchedulerControlManager>,
|
scheduler_control: web::Data<crate::server::scheduler::SharedSchedulerControlManager>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let rx = state.get_ref().clone();
|
let rx = state.get_ref().clone();
|
||||||
@@ -248,6 +250,31 @@ pub async fn events(
|
|||||||
initial.status.freq.hz,
|
initial.status.freq.hz,
|
||||||
&format!("{:?}", initial.status.mode),
|
&format!("{:?}", initial.status.mode),
|
||||||
);
|
);
|
||||||
|
if scheduler_control.scheduler_allowed() {
|
||||||
|
let desired = {
|
||||||
|
let map = scheduler_status.read().unwrap_or_else(|e| e.into_inner());
|
||||||
|
map.get(rid)
|
||||||
|
.filter(|status| status.active)
|
||||||
|
.map(|status| {
|
||||||
|
status
|
||||||
|
.last_bookmark_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|bookmark_id| {
|
||||||
|
bookmark_store.get(bookmark_id).map(|bookmark| {
|
||||||
|
(
|
||||||
|
bookmark_id.clone(),
|
||||||
|
bookmark.freq_hz,
|
||||||
|
bookmark.mode,
|
||||||
|
bookmark.bandwidth_hz.unwrap_or(0) as u32,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
vchan_mgr.sync_scheduler_channels(rid, &desired);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the prefix burst: rig state → session UUID → initial channels.
|
// Build the prefix burst: rig state → session UUID → initial channels.
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ struct InternalChannel {
|
|||||||
/// Audio filter bandwidth in Hz (0 = mode default).
|
/// Audio filter bandwidth in Hz (0 = mode default).
|
||||||
bandwidth_hz: u32,
|
bandwidth_hz: u32,
|
||||||
permanent: bool,
|
permanent: bool,
|
||||||
|
scheduler_bookmark_id: Option<String>,
|
||||||
/// Session UUIDs currently subscribed to this channel.
|
/// Session UUIDs currently subscribed to this channel.
|
||||||
session_ids: Vec<Uuid>,
|
session_ids: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
@@ -160,6 +161,7 @@ impl ClientChannelManager {
|
|||||||
mode: mode.to_string(),
|
mode: mode.to_string(),
|
||||||
bandwidth_hz: 0,
|
bandwidth_hz: 0,
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
scheduler_bookmark_id: None,
|
||||||
session_ids: Vec::new(),
|
session_ids: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,6 +229,7 @@ impl ClientChannelManager {
|
|||||||
mode: mode.to_string(),
|
mode: mode.to_string(),
|
||||||
bandwidth_hz: 0,
|
bandwidth_hz: 0,
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
scheduler_bookmark_id: None,
|
||||||
session_ids: vec![session_id],
|
session_ids: vec![session_id],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -476,6 +479,121 @@ impl ClientChannelManager {
|
|||||||
pub fn session_channel(&self, session_id: Uuid) -> Option<(String, Uuid)> {
|
pub fn session_channel(&self, session_id: Uuid) -> Option<(String, Uuid)> {
|
||||||
self.sessions.read().unwrap().get(&session_id).cloned()
|
self.sessions.read().unwrap().get(&session_id).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reconcile visible scheduler-managed channels for a rig.
|
||||||
|
///
|
||||||
|
/// These channels are user-visible virtual channels sourced from the
|
||||||
|
/// scheduler's currently active extra bookmarks. They are kept separate
|
||||||
|
/// from user-allocated channels so connect-time sync can materialise them
|
||||||
|
/// without duplicating arbitrary user state.
|
||||||
|
pub fn sync_scheduler_channels(
|
||||||
|
&self,
|
||||||
|
rig_id: &str,
|
||||||
|
desired: &[(String, u64, String, u32)],
|
||||||
|
) {
|
||||||
|
let mut rigs = self.rigs.write().unwrap();
|
||||||
|
let Some(channels) = rigs.get_mut(rig_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
let desired_map: HashMap<String, (u64, String, u32)> = desired
|
||||||
|
.iter()
|
||||||
|
.map(|(bookmark_id, freq_hz, mode, bandwidth_hz)| {
|
||||||
|
(bookmark_id.clone(), (*freq_hz, mode.clone(), *bandwidth_hz))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let desired_ids: std::collections::HashSet<&str> =
|
||||||
|
desired_map.keys().map(String::as_str).collect();
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
while idx < channels.len() {
|
||||||
|
let remove = if let Some(bookmark_id) = channels[idx].scheduler_bookmark_id.as_deref() {
|
||||||
|
!desired_ids.contains(bookmark_id) && channels[idx].session_ids.is_empty()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if remove {
|
||||||
|
let channel_id = channels[idx].id;
|
||||||
|
channels.remove(idx);
|
||||||
|
self.send_audio_cmd(VChanAudioCmd::Remove(channel_id));
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for channel in channels.iter_mut() {
|
||||||
|
let Some(bookmark_id) = channel.scheduler_bookmark_id.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some((freq_hz, mode, bandwidth_hz)) = desired_map.get(bookmark_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if channel.freq_hz != *freq_hz {
|
||||||
|
channel.freq_hz = *freq_hz;
|
||||||
|
self.send_audio_cmd(VChanAudioCmd::SetFreq {
|
||||||
|
uuid: channel.id,
|
||||||
|
freq_hz: *freq_hz,
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if channel.mode != *mode {
|
||||||
|
channel.mode = mode.clone();
|
||||||
|
self.send_audio_cmd(VChanAudioCmd::SetMode {
|
||||||
|
uuid: channel.id,
|
||||||
|
mode: mode.clone(),
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if channel.bandwidth_hz != *bandwidth_hz {
|
||||||
|
channel.bandwidth_hz = *bandwidth_hz;
|
||||||
|
self.send_audio_cmd(VChanAudioCmd::SetBandwidth {
|
||||||
|
uuid: channel.id,
|
||||||
|
bandwidth_hz: *bandwidth_hz,
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (bookmark_id, freq_hz, mode, bandwidth_hz) in desired {
|
||||||
|
let exists = channels.iter().any(|channel| {
|
||||||
|
channel.scheduler_bookmark_id.as_deref() == Some(bookmark_id.as_str())
|
||||||
|
});
|
||||||
|
if exists {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if channels.len() >= self.max_channels {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let channel_id = Uuid::new_v4();
|
||||||
|
channels.push(InternalChannel {
|
||||||
|
id: channel_id,
|
||||||
|
freq_hz: *freq_hz,
|
||||||
|
mode: mode.clone(),
|
||||||
|
bandwidth_hz: *bandwidth_hz,
|
||||||
|
permanent: false,
|
||||||
|
scheduler_bookmark_id: Some(bookmark_id.clone()),
|
||||||
|
session_ids: Vec::new(),
|
||||||
|
});
|
||||||
|
self.send_audio_cmd(VChanAudioCmd::Subscribe {
|
||||||
|
uuid: channel_id,
|
||||||
|
freq_hz: *freq_hz,
|
||||||
|
mode: mode.clone(),
|
||||||
|
});
|
||||||
|
if *bandwidth_hz > 0 {
|
||||||
|
self.send_audio_cmd(VChanAudioCmd::SetBandwidth {
|
||||||
|
uuid: channel_id,
|
||||||
|
bandwidth_hz: *bandwidth_hz,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
self.broadcast_change(rig_id, channels);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -502,4 +620,24 @@ mod tests {
|
|||||||
assert!(channels.iter().all(|ch| ch.id != channel.id));
|
assert!(channels.iter().all(|ch| ch.id != channel.id));
|
||||||
assert!(mgr.session_channel(session_id).is_none());
|
assert!(mgr.session_channel(session_id).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_scheduler_channels_materializes_visible_scheduler_channels() {
|
||||||
|
let mgr = ClientChannelManager::new(4);
|
||||||
|
let rig_id = "rig-a";
|
||||||
|
|
||||||
|
mgr.init_rig(rig_id, 14_074_000, "USB");
|
||||||
|
mgr.sync_scheduler_channels(
|
||||||
|
rig_id,
|
||||||
|
&[("bm-ft8".to_string(), 14_074_000, "DIG".to_string(), 3_000)],
|
||||||
|
);
|
||||||
|
|
||||||
|
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, "DIG");
|
||||||
|
assert_eq!(channels[1].bandwidth_hz, 3_000);
|
||||||
|
assert_eq!(channels[1].subscribers, 0);
|
||||||
|
assert!(!channels[1].permanent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user