melvin_ob/mode_control/
base_mode.rs

1use super::mode_context::ModeContext;
2use super::signal::{
3    PeriodicImagingEndSignal,
4    TaskEndSignal::{self, Join, Timestamp},
5};
6use crate::flight_control::{FlightComputer, FlightState, orbit::IndexedOrbitPosition};
7use crate::imaging::CameraAngle;
8use crate::objective::BeaconControllerState;
9use crate::scheduling::{EndCondition, TaskController, task::SwitchStateTask};
10use crate::{DT_0_STD, error, fatal, info, log};
11use chrono::{DateTime, TimeDelta, Utc};
12use std::{future::Future, pin::Pin, sync::Arc};
13use strum_macros::Display;
14use tokio::{sync::oneshot, task::JoinHandle, time::Instant};
15use tokio_util::sync::CancellationToken;
16
17/// Represents high-level operational modes of the onboard software when in orbit.
18/// Each variant encodes different scheduling logic and task handling behavior.
19#[derive(Display, Clone, Copy)]
20pub(super) enum BaseMode {
21    /// Regular mapping mode focused on maximizing imaging coverage.
22    MappingMode,
23    /// Mode dedicated to get full communications mode coverage while still mapping when possible.
24    BeaconObjectiveScanningMode,
25}
26
27impl BaseMode {
28    /// Default camera angle used during mapping operations.
29    const DEF_MAPPING_ANGLE: CameraAngle = CameraAngle::Narrow;
30
31    /// Executes a full mapping acquisition cycle, listening until either a signal or cancellation occurs.
32    ///
33    /// This function initializes an image acquisition cycle using the default mapping camera angle
34    /// and coordinates between the camera controller and various signal channels.
35    /// It finalizes by marking orbit coverage and exporting updated coverage data.
36    ///
37    /// # Arguments
38    /// - `context`: A shared reference to a [`ModeContext`] object.
39    /// - `end`: A [`TaskEndSignal`]-enum type indicating how the task end condition should be defined.
40    /// - `c_tok`: A [`CancellationToken`] that is able to cancel this task with proper cleanup.
41    #[allow(clippy::cast_possible_wrap)]
42    async fn exec_map(context: Arc<ModeContext>, end: TaskEndSignal, c_tok: CancellationToken) {
43        let end_t = {
44            match end {
45                Timestamp(dt) => dt,
46                Join(_) => Utc::now() + TimeDelta::seconds(10000),
47            }
48        };
49        let o_ch_clone = context.o_ch_clone().await;
50        let acq_phase = {
51            let f_cont_lock = Arc::clone(&context.k().f_cont());
52            let (tx, rx) = oneshot::channel();
53            let i_start = o_ch_clone.i_entry().new_from_pos(f_cont_lock.read().await.current_pos());
54            let k_clone = Arc::clone(context.k());
55            let img_dt = o_ch_clone.img_dt();
56            FlightComputer::set_angle_wait(Arc::clone(&f_cont_lock), Self::DEF_MAPPING_ANGLE).await;
57            let handle = tokio::spawn(async move {
58                k_clone
59                    .c_cont()
60                    .execute_acquisition_cycle(
61                        f_cont_lock,
62                        k_clone.con(),
63                        (end_t, rx),
64                        img_dt,
65                        i_start.index(),
66                    )
67                    .await
68            });
69            (handle, tx)
70        };
71
72        let ranges = {
73            if let Join(join_handle) = end {
74                tokio::pin!(join_handle);
75                tokio::select! {
76                    () = c_tok.cancelled() => {
77                        let sig = PeriodicImagingEndSignal::KillNow;
78                        acq_phase.1.send(sig).unwrap_or_else(|_|fatal!("Receiver hung up!"));
79                        join_handle.abort();
80                        acq_phase.0.await.ok().unwrap_or(vec![(0, 0)])
81                    },
82                    _ = &mut join_handle => {
83                        let sig = PeriodicImagingEndSignal::KillLastImage;
84                        acq_phase.1.send(sig).unwrap_or_else(|_|fatal!("Receiver hung up!"));
85                        acq_phase.0.await.ok().unwrap_or(vec![(0, 0)])
86                    }
87                }
88            } else {
89                let img_fut = acq_phase.0;
90                tokio::pin!(img_fut);
91                tokio::select! {
92                    () = c_tok.cancelled() => {
93                        let sig = PeriodicImagingEndSignal::KillNow;
94                        acq_phase.1.send(sig).expect("[FATAL] Receiver hung up!");
95                        img_fut.await.ok().unwrap_or(vec![(0, 0)])
96                    }
97                    res = &mut img_fut => {
98                        res.ok().unwrap_or(vec![(0, 0)])
99                    }
100                }
101            }
102        };
103        let fixed_ranges =
104            IndexedOrbitPosition::map_ranges(&ranges, o_ch_clone.i_entry().period() as isize);
105        let and = if let Some(r) = ranges.get(1) {
106            format!(" and {} - {}", r.0, r.1)
107        } else {
108            String::new()
109        };
110        log!("Marking done: {} - {}{and}", ranges[0].0, ranges[0].1);
111        let k_loc = Arc::clone(context.k());
112        let c_orbit_lock = k_loc.c_orbit();
113        let mut c_orbit = c_orbit_lock.write().await;
114        for (start, end) in &fixed_ranges {
115            if start != end {
116                c_orbit.mark_done(*start, *end);
117            }
118        }
119        log!(
120            "Current discrete Orbit Coverage is {}%.",
121            c_orbit.get_coverage() * 100
122        );
123        c_orbit.try_export_default();
124    }
125
126    /// Listens for Beacon Objective communication pings until a timeout or cancellation.
127    ///
128    /// Uses an event-based listener to process incoming beacon messages.
129    /// Automatically terminates based on task completion or shutdown signals.
130    ///
131    /// # Arguments
132    /// - `context`: A shared reference to a `ModeContext` object.
133    /// - `end`: A `TaskEndSignal`-enum type indicating how the task end condition should be defined.
134    /// - `c_tok`: A `CancellationToken` that is able to cancel this task with proper cleanup.
135    async fn exec_comms(context: Arc<ModeContext>, end: TaskEndSignal, c_tok: CancellationToken) {
136        let mut event_rx = context.super_v().subscribe_event_hub();
137
138        let mut fut: Pin<Box<dyn Future<Output = ()> + Send>> = match end {
139            Timestamp(t) => {
140                let due_secs = (t - Utc::now()).to_std().unwrap_or(DT_0_STD);
141                Box::pin(tokio::time::sleep_until(Instant::now() + due_secs))
142            }
143            Join(join_handle) => Box::pin(async { join_handle.await.ok().unwrap() }),
144        };
145
146        let start = Utc::now();
147        info!("Starting Comms Listener.");
148        loop {
149            tokio::select! {
150                // Wait for a message
151                Ok(msg) = event_rx.recv() => {
152                    let f_cont = context.k().f_cont();
153                    context.beac_cont().handle_poss_bo_ping(msg, f_cont).await;
154                }
155                // If the timeout expires, exit
156                () = &mut fut => {
157                    log!("Comms Deadline reached after {}s. Stopping listener.",
158                    (Utc::now() - start).num_seconds());
159                    break;
160                },
161                // If the task gets cancelled exit with the updated beacon vector
162                () = c_tok.cancelled() => {
163                    log!("Comms Listener cancelled. Stopping listener.");
164                    break;
165                }
166            }
167        }
168    }
169
170    /// Ensures any required preconditions for the current mode are satisfied before scheduling begins.
171    ///
172    /// # Arguments
173    /// - `context`: A shared reference to a `ModeContext` object.
174    ///
175    /// # Returns
176    /// A `DateTime<Utc>` indicating the time when scheduled tasks should start.
177    pub(super) async fn handle_sched_preconditions(
178        &self,
179        context: Arc<ModeContext>,
180    ) -> DateTime<Utc> {
181        match self {
182            BaseMode::MappingMode => FlightComputer::escape_if_comms(context.k().f_cont()).await,
183            BaseMode::BeaconObjectiveScanningMode => {
184                FlightComputer::get_to_comms(context.k().f_cont()).await
185            }
186        }
187    }
188
189    /// Returns a handle to the scheduling task corresponding to the current operational `BaseMode`-variant.
190    ///
191    /// - For `MappingMode`, a standard optimal orbit scheduler is used.
192    /// - For `BeaconObjectiveScanningMode`, a communications-aware scheduler is launched.
193    ///
194    /// Depending on the current flight state, this will also launch a mapping or
195    /// beacon listening task to run concurrently.
196    ///
197    /// # Arguments
198    /// - `context`: A shared reference to a `ModeContext` object.
199    /// - `c_tok`: A `CancellationToken` that is able to cancel this task with proper cleanup.
200    /// - `comms_end`: A `DateTime<Utc>` indicating the end of the current comms cycle when in `BeaconObjectiveScanningMode`.
201    /// - `end`: An optional `EndCondition` type if i.e. a burn sequence follows to this task list.
202    ///
203    /// # Returns
204    /// A `JoinHandle<()` to join with the scheduling task
205    #[must_use]
206    pub(super) async fn get_schedule_handle(
207        &self,
208        context: Arc<ModeContext>,
209        c_tok: CancellationToken,
210        comms_end: DateTime<Utc>,
211        end: Option<EndCondition>,
212    ) -> JoinHandle<()> {
213        let k = Arc::clone(context.k());
214        let o_ch = context.o_ch_clone().await;
215        let j_handle = match self {
216            BaseMode::MappingMode => tokio::spawn(TaskController::sched_opt_orbit(
217                k.t_cont(),
218                k.c_orbit(),
219                k.f_cont(),
220                o_ch.i_entry(),
221                end,
222            )),
223            BaseMode::BeaconObjectiveScanningMode => {
224                let last_obj_end =
225                    context.beac_cont().last_active_beac_end().await.unwrap_or(Utc::now());
226                tokio::spawn(TaskController::sched_opt_orbit_w_comms(
227                    k.t_cont(),
228                    k.c_orbit(),
229                    k.f_cont(),
230                    o_ch.i_entry(),
231                    last_obj_end,
232                    comms_end,
233                    end,
234                ))
235            }
236        };
237        let state = context.k().f_cont().read().await.state();
238        if state == FlightState::Acquisition {
239            tokio::spawn(BaseMode::exec_map(context, Join(j_handle), c_tok))
240        } else if state == FlightState::Comms {
241            match self {
242                BaseMode::MappingMode => fatal!("Illegal state ({state})!"),
243                BaseMode::BeaconObjectiveScanningMode => {
244                    tokio::spawn(BaseMode::exec_comms(context, Join(j_handle), c_tok))
245                }
246            }
247        } else {
248            j_handle
249        }
250    }
251
252    /// Spawns the corresponding primitive for the task wait time.
253    ///
254    /// The returned handle either:
255    /// - `FlightState::Charge`: Waits for the task timeout and exports a full image snapshot.
256    /// - `FlightState::Acquisition`: Executes a mapping task.
257    /// - `FlightState::Comms`: Executes a beacon listening task.
258    ///
259    /// This is useful for blocking execution while ensuring mode-specific behavior continues.
260    ///
261    /// # Arguments
262    /// - `context`: A shared reference to a `ModeContext` object.
263    /// - `due`: A `DateTime<Utc>` indicating the task wait timeout time.
264    /// - `c_tok`: A `CancellationToken` that is able to cancel the spawned task with proper cleanup.
265    ///
266    /// # Returns
267    /// A `JoinHandle<()` to join with the state specific primitive.
268    #[must_use]
269    pub(super) async fn get_wait(
270        &self,
271        context: Arc<ModeContext>,
272        due: DateTime<Utc>,
273        c_tok: CancellationToken,
274    ) -> JoinHandle<()> {
275        let (current_state, _) = {
276            let f_cont = context.k().f_cont();
277            let lock = f_cont.read().await;
278            (lock.state(), lock.client())
279        };
280        let c_tok_clone = c_tok.clone();
281        let def = Box::pin(async move {
282            let sleep = (due - Utc::now()).to_std().unwrap_or(DT_0_STD);
283            tokio::time::timeout(sleep, c_tok_clone.cancelled()).await.ok().unwrap_or(());
284        });
285        let task_fut: Pin<Box<dyn Future<Output = _> + Send>> = match current_state {
286            FlightState::Charge => def,
287            FlightState::Acquisition => Box::pin(async move {
288                Self::exec_map(context, Timestamp(due), c_tok).await;
289            }),
290            FlightState::Comms => {
291                if let Self::BeaconObjectiveScanningMode = self {
292                    Box::pin(async move {
293                        Self::exec_comms(context, Timestamp(due), c_tok).await;
294                    })
295                } else {
296                    error!("Not in Beacon Objective Scanning Mode. Waiting for Comms to end.");
297                    def
298                }
299            }
300            _ => {
301                fatal!("Illegal state ({current_state})!")
302            }
303        };
304        tokio::spawn(task_fut)
305    }
306
307    /// Executes the corresponding primitive for task execution.
308    ///
309    /// In `GlobalMode` with a corresponding [`BaseMode`] this handles the logic for [`SwitchStateTask`].
310    ///
311    /// # Arguments
312    /// - `context`: A shared reference to a [`ModeContext`] object.
313    /// - `task`: The corresponding [`SwitchStateTask`] object.
314    pub(super) async fn get_task(&self, context: Arc<ModeContext>, task: SwitchStateTask) {
315        let f_cont = context.k().f_cont();
316        match task.target_state() {
317            FlightState::Acquisition => {
318                FlightComputer::set_state_wait(f_cont, FlightState::Acquisition).await;
319            }
320            FlightState::Charge => {
321                let task_handle = async {
322                    FlightComputer::set_state_wait(f_cont, FlightState::Charge).await;
323                };
324                let k_clone = Arc::clone(context.k());
325                let export_handle = tokio::spawn(async move {
326                    let c_cont = k_clone.c_cont();
327                    c_cont
328                        .export_full_snapshot()
329                        .await
330                        .unwrap_or_else(|_| fatal!("Export failed!"));
331                    c_cont.create_thumb_snapshot().await.unwrap_or_else(|e| {
332                        error!("Error exporting thumb snapshot: {e}.");
333                    });
334                });
335                task_handle.await;
336                if export_handle.is_finished() {
337                    export_handle.await.unwrap();
338                } else {
339                    error!("Couldnt finish Map export!");
340                    export_handle.abort();
341                }
342            }
343            FlightState::Comms => match self {
344                BaseMode::MappingMode => {
345                    fatal!("Illegal target state!")
346                }
347                BaseMode::BeaconObjectiveScanningMode => {
348                    FlightComputer::set_state_wait(f_cont, FlightState::Comms).await;
349                }
350            },
351            _ => fatal!("Illegal target state!"),
352        }
353    }
354
355    /// Returns the relevant `BeaconControllerState` associated with this mode.
356    ///
357    /// Used to inform beacon-handling logic of the signal that would indicate switching.
358    pub(super) fn get_rel_bo_event(self) -> BeaconControllerState {
359        match self {
360            BaseMode::MappingMode => BeaconControllerState::ActiveBeacons,
361            BaseMode::BeaconObjectiveScanningMode => BeaconControllerState::NoActiveBeacons,
362        }
363    }
364
365    /// Returns the new `BaseMode` following a Beacon Objective Event.
366    ///
367    /// Used to inform beacon-handling logic of the new state after a signal.
368    pub(super) fn bo_event(self) -> Self {
369        match self {
370            BaseMode::MappingMode => Self::BeaconObjectiveScanningMode,
371            BaseMode::BeaconObjectiveScanningMode => Self::MappingMode,
372        }
373    }
374}