melvin_ob/mode_control/mode/global_mode.rs
1use crate::objective::{BeaconControllerState, KnownImgObjective};
2use crate::scheduling::task::Task;
3use crate::mode_control::{
4 base_mode::BaseMode,
5 mode_context::ModeContext,
6 signal::{ExecExitSignal, OpExitSignal, WaitExitSignal, OptOpExitSignal},
7};
8use crate::{DT_0_STD, fatal, info, log, warn};
9use async_trait::async_trait;
10use chrono::{DateTime, TimeDelta, Utc};
11use std::mem::discriminant;
12use std::{future::Future, pin::Pin, sync::Arc};
13use tokio::{sync::{RwLock, watch::Receiver}, task::JoinError};
14use tokio_util::sync::CancellationToken;
15
16/// Trait representing a high-level operational mode within the onboard Finite-State-Machine (FSM) architecture.
17/// Implementors of [`GlobalMode`] encapsulate full behavioral logic for mode-specific task scheduling,
18/// signal handling, and state transitions.
19#[async_trait]
20pub trait GlobalMode: Sync + Send {
21 /// Returns the rationale string for finishing the current phase due to safe mode entry.
22 fn safe_mode_rationale(&self) -> &'static str { "SAFE mode Event!" }
23 /// Returns the rationale string for finishing the current phase due to a new Zoned Objective.
24 fn new_zo_rationale(&self) -> &'static str { "newly discovered ZO!" }
25 /// Returns the rationale string for finishing the current phase due to a new Beacon Objective.
26 fn new_bo_rationale(&self) -> &'static str { "newly discovered BO!" }
27 /// Returns the rationale string used when the task queue has completed.
28 fn tasks_done_rationale(&self) -> &'static str { "tasks list done!" }
29 /// Returns the rationale for finishing the task queue and exiting the orbit for ZO Retrieval.
30 fn tasks_done_exit_rationale(&self) -> &'static str {
31 "tasks list done and exited orbit for ZO Retrieval!"
32 }
33 /// Returns the rationale for finishing the current phase due to being outside of orbit without a valid reason.
34 fn out_of_orbit_rationale(&self) -> &'static str { "out of orbit without purpose!" }
35 /// Returns the rationale used for finishing the current phase when a beacon objective has been completed or expired.
36 fn bo_done_rationale(&self) -> &'static str { "BO done or expired!" }
37
38 /// Returns the string representation of the current mode.
39 fn type_name(&self) -> &'static str;
40
41 /// Initializes the mode with the provided context.
42 ///
43 /// # Arguments
44 /// * `context` - Shared reference to the current mode context.
45 ///
46 /// # Returns
47 /// [`OpExitSignal`] - Signal indicating what action to take after initialization.
48 async fn init_mode(&self, context: Arc<ModeContext>) -> OpExitSignal;
49
50 /// Executes all tasks in the current task queue in sequence.
51 /// Waits for each task’s scheduled time and handles early exit signals such as safe transitions or new objectives.
52 ///
53 /// # Arguments
54 /// * `context` - Shared reference to the current mode context.
55 ///
56 /// # Returns
57 /// * [`OpExitSignal`] - Signal indicating whether to continue or exit the mode.
58 #[allow(clippy::cast_sign_loss, clippy::cast_precision_loss)]
59 async fn exec_task_queue(&self, context: Arc<ModeContext>) -> OpExitSignal {
60 let context_local = Arc::clone(&context);
61 let mut tasks = 0;
62 while let Some(task) = {
63 let sched_arc = context_local.k().t_cont().sched_arc();
64 let mut sched_lock = sched_arc.write().await;
65 let t = sched_lock.pop_front();
66 drop(sched_lock);
67 t
68 } {
69 let due_time = task.t() - Utc::now();
70 let task_type = task.task_type();
71 info!("TASK {tasks}: {task_type} in {}s!", due_time.num_seconds());
72 while task.t() > Utc::now() + TimeDelta::seconds(2) {
73 let context_clone = Arc::clone(&context);
74 match self.exec_task_wait(context_clone, task.t()).await {
75 WaitExitSignal::Continue => {}
76 WaitExitSignal::SafeEvent => {
77 return self.safe_handler(context_local).await;
78 }
79 WaitExitSignal::NewZOEvent(obj) => {
80 if let Some(opt) = self.zo_handler(&context, obj).await {
81 return opt;
82 };
83 }
84 WaitExitSignal::BOEvent => {
85 if let Some(opt) = self.bo_event_handler(&context).await {
86 return opt;
87 };
88 }
89 };
90 }
91 let task_delay = (task.t() - Utc::now()).num_milliseconds() as f32 / 1000.0;
92 if task_delay.abs() > 2.0 {
93 log!("Task {tasks} delayed by {task_delay}s!");
94 }
95 let context_clone = Arc::clone(&context);
96 match self.exec_task(context_clone, task).await {
97 ExecExitSignal::Continue => {}
98 ExecExitSignal::SafeEvent => {
99 return self.safe_handler(context_local).await;
100 }
101 ExecExitSignal::NewZOEvent(_) => {
102 fatal!("Unexpected task exit signal!");
103 }
104 };
105 tasks += 1;
106 }
107 OpExitSignal::Continue
108 }
109
110 /// Waits until a task’s due time or handles early exit signals while executing a wait primitive.
111 ///
112 /// # Arguments
113 /// * `context` - Shared reference to the mode context.
114 /// * `due` - Scheduled task start time.
115 ///
116 /// # Returns
117 /// * `WaitExitSignal` - Resulting signal from monitoring exit conditions.
118 async fn exec_task_wait(&self, context: Arc<ModeContext>, due: DateTime<Utc>)
119 -> WaitExitSignal;
120
121 /// Executes a single task and returns a signal indicating whether to continue or exit.
122 ///
123 /// # Arguments
124 /// * `context` - Shared reference to the mode context.
125 /// * `task` - Task to be executed.
126 ///
127 /// # Returns
128 /// * `ExecExitSignal` - Resulting signal after task execution.
129 async fn exec_task(&self, context: Arc<ModeContext>, task: Task) -> ExecExitSignal;
130
131 /// Handles unplanned safe mode transition.
132 ///
133 /// # Arguments
134 /// * `context` - Shared reference to the mode context.
135 ///
136 /// # Returns
137 /// * `OpExitSignal` - Signal after executing safe-mode exit logic.
138 async fn safe_handler(&self, context: Arc<ModeContext>) -> OpExitSignal;
139
140 /// Handles the reception of a new Zoned Objective (ZO).
141 ///
142 /// # Arguments
143 /// * `context` - Shared reference to the mode context.
144 /// * `obj` - The new `KnownImgObjective` that triggered the transition.
145 ///
146 /// # Returns
147 /// * `OptOpExitSignal` - Optional signal indicating a mode switch or continuation.
148 async fn zo_handler(
149 &self,
150 context: &Arc<ModeContext>,
151 obj: KnownImgObjective,
152 ) -> OptOpExitSignal;
153
154 /// Handles beacon-related events (e.g., mode changes or new signals).
155 ///
156 /// # Arguments
157 /// * `context` - Shared reference to the mode context.
158 ///
159 /// # Returns
160 /// * `OptOpExitSignal` - Optional signal indicating a mode switch or continuation.
161 async fn bo_event_handler(&self, context: &Arc<ModeContext>) -> OptOpExitSignal;
162
163 /// Handles cleanup and transition logic when exiting a mode.
164 ///
165 /// # Arguments
166 /// * `context` - Shared reference to the mode context.
167 ///
168 /// # Returns
169 /// * `Box<dyn GlobalMode>` - The next mode to transition into.
170 async fn exit_mode(&self, context: Arc<ModeContext>) -> Box<dyn GlobalMode>;
171}
172
173/// An internal extension trait for [`GlobalMode`] that encapsulates logic specific to
174/// time-constrained orbital task execution.
175///
176/// The [`OrbitalMode`] trait provides utility methods for task waiting, event monitoring,
177/// and logging transitions related to orbital activities, such as beacon detection or
178/// zoned objective events.
179///
180/// This trait is not intended to be used directly outside the mode control subsystem.
181pub(super) trait OrbitalMode: GlobalMode {
182 /// Returns the maximum duration allowed for scheduled task waiting.
183 /// If the remaining time until a task is due exceeds this value, the mode may begin
184 /// long-wait procedures; otherwise, a fallback short sleep is used.
185 ///
186 /// # Returns
187 /// * [`TimeDelta`] – Maximum duration (default: 10 seconds).
188 fn get_max_dt() -> TimeDelta { TimeDelta::seconds(10) }
189
190 /// Returns a reference to the [`BaseMode`] associated with this orbital mode.
191 ///
192 /// This is used for delegating tasks and behavior such as wait and scheduling primitives.
193 ///
194 /// # Returns
195 /// * `&BaseMode` – The current mode (e.g., Mapping or Beacon Objective Scanning).
196 fn base(&self) -> &BaseMode;
197
198 /// Waits until a scheduled task’s due time or returns early if a notable event occurs.
199 ///
200 /// While waiting, this method concurrently monitors for:
201 /// - Safe mode triggers
202 /// - New zoned objectives (ZO)
203 /// - Beacon state changes (BO)
204 ///
205 /// It also supports short or long sleep strategies depending on how far the task lies in the future.
206 ///
207 /// # Arguments
208 /// * `context` – Shared reference to the current [`ModeContext`].
209 /// * `due` – Scheduled time at which the next task is expected to start.
210 ///
211 /// # Returns
212 /// * [`WaitExitSignal`] – An event indicating why the wait ended (safe event, ZO, or BO, ...).
213 async fn exec_task_wait(
214 &self,
215 context: Arc<ModeContext>,
216 due: DateTime<Utc>,
217 ) -> WaitExitSignal {
218 let safe_mon = context.super_v().safe_mon();
219 let mut zo_mon = context.zo_mon().write().await;
220 let bo_mon = context.bo_mon();
221 let cancel_task = CancellationToken::new();
222
223 let fut: Pin<Box<dyn Future<Output = Result<_, JoinError>> + Send>> =
224 if (due - Utc::now()) > Self::get_max_dt() {
225 Box::pin(self.base().get_wait(Arc::clone(&context), due, cancel_task.clone()).await)
226 } else {
227 warn!("Task wait time too short. Just waiting!");
228 Box::pin(async {
229 let sleep = (due - Utc::now()).to_std().unwrap_or(DT_0_STD);
230 tokio::time::timeout(sleep, cancel_task.cancelled()).await.ok().unwrap_or(());
231 Ok(())
232 })
233 };
234 let bo_change_signal = self.base().get_rel_bo_event();
235 tokio::pin!(fut);
236 tokio::select! {
237 exit_sig = &mut fut => {
238 exit_sig.unwrap_or_else(|_|fatal!("Task wait hung up!"));
239 WaitExitSignal::Continue
240 },
241 () = safe_mon.notified() => {
242 cancel_task.cancel();
243 fut.await.ok();
244 WaitExitSignal::SafeEvent
245 },
246 msg = zo_mon.recv() => {
247 let img_obj = msg.unwrap_or_else(||fatal!("Objective monitor wait hung up!"));
248 cancel_task.cancel();
249 fut.await.ok();
250 WaitExitSignal::NewZOEvent(img_obj)
251 }
252 () = Self::monitor_bo_mon_change(bo_change_signal, bo_mon) => {
253 cancel_task.cancel();
254 fut.await.ok();
255 WaitExitSignal::BOEvent
256 }
257
258 }
259 }
260
261 /// Continuously monitors the [`BeaconControllerState`] until it changes to the expected value.
262 ///
263 /// Used to react to asynchronous events in which the beacon scanning mode (e.g., active or inactive)
264 /// must trigger a response in the scheduler or mode transition logic.
265 ///
266 /// # Arguments
267 /// * `sig` – The [`BeaconControllerState`] state to wait for.
268 /// * `bo_mon` – A watch receiver providing asynchronous access to beacon state changes.
269 async fn monitor_bo_mon_change(
270 sig: BeaconControllerState,
271 bo_mon: &RwLock<Receiver<BeaconControllerState>>,
272 ) {
273 let mut bo_mon_lock = bo_mon.write().await;
274 loop {
275 if let Ok(()) = bo_mon_lock.changed().await {
276 let sent_sig = bo_mon_lock.borrow_and_update();
277 let sent_sig_ref = &*sent_sig;
278 if discriminant(&sig) == discriminant(sent_sig_ref) {
279 return;
280 }
281 }
282 }
283 }
284
285 /// Logs a beacon-related event and finalizes the orbit at the current satellite position.
286 ///
287 /// This is used to capture the reason for switching out of the current [`BaseMode`],
288 /// typically due to either beacon objective completion or expiry.
289 ///
290 /// # Arguments
291 /// * `context` – Shared reference to the current [`ModeContext`].
292 /// * `base` – The [`BaseMode`] that determines the rationale for finishing the orbit segment.
293 async fn log_bo_event(&self, context: &Arc<ModeContext>, base: BaseMode) {
294 match base {
295 BaseMode::BeaconObjectiveScanningMode => context.o_ch_lock().write().await.finish(
296 context.k().f_cont().read().await.current_pos(),
297 self.new_bo_rationale(),
298 ),
299 BaseMode::MappingMode => context.o_ch_lock().write().await.finish(
300 context.k().f_cont().read().await.current_pos(),
301 self.bo_done_rationale(),
302 ),
303 }
304 }
305}