melvin_ob/mode_control/mode/
in_orbit_mode.rs

1use crate::scheduling::task::{BaseTask, Task};
2use crate::objective::KnownImgObjective;
3use crate::flight_control::FlightComputer;
4use super::{zo_prep_mode::ZOPrepMode, global_mode::{GlobalMode, OrbitalMode}};
5use crate::mode_control::{
6    base_mode::BaseMode,
7    mode_context::ModeContext,
8    signal::{ExecExitSignal, OpExitSignal, WaitExitSignal, OptOpExitSignal},
9};
10use crate::{fatal, obj, warn};
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use std::sync::Arc;
14use tokio_util::sync::CancellationToken;
15
16/// [`InOrbitMode`] is an implementation of [`GlobalMode`] and [`OrbitalMode`] that governs normal
17/// in-orbit operations such as transitioning between flight states, listening for event-driven
18/// interrupts (e.g., Safe Mode, ZO, BO), and executing scheduled task queues specific to the [`InOrbitMode`].
19///
20/// This mode represents the satellite operating in a stable orbital state, performing mapping or communication tasks.
21///
22/// The mode is interruptible and reactive to new objectives, flight events, or beacon detections.
23#[derive(Clone)]
24pub struct InOrbitMode {
25    /// The base operational context (e.g. Mapping or Beacon Objective Scanning).
26    base: BaseMode,
27}
28
29impl InOrbitMode {
30    /// The internal name of the mode used for logging and identification.
31    const MODE_NAME: &'static str = "InOrbitMode";
32
33    /// Constructs a new [`InOrbitMode`] instance using the given [`BaseMode`].
34    ///
35    /// # Arguments
36    /// * `base` – The high-level operational context (e.g., `MappingMode`).
37    ///
38    /// # Returns
39    /// * [`InOrbitMode`] – The initialized mode.
40    pub fn new(base: BaseMode) -> Self { Self { base } }
41}
42
43impl OrbitalMode for InOrbitMode {
44    /// Returns a reference to the current [`BaseMode`] context.
45    fn base(&self) -> &BaseMode { &self.base }
46}
47
48#[async_trait]
49impl GlobalMode for InOrbitMode {
50    /// Returns the static name of this mode.
51    fn type_name(&self) -> &'static str { Self::MODE_NAME }
52
53    /// Initializes the mode by running scheduling logic and listening for early exit signals.
54    ///
55    /// Reacts to Safe Mode signals and reinitializes if needed. Otherwise, continues
56    /// with task list execution.
57    ///
58    /// # Arguments
59    /// * `context` – The shared execution context for the mode.
60    ///
61    /// # Returns
62    /// * [`OpExitSignal`] – Signal indicating if the mode should continue or reinitialize.
63    async fn init_mode(&self, context: Arc<ModeContext>) -> OpExitSignal {
64        let cancel_task = CancellationToken::new();
65        let comms_end = self.base.handle_sched_preconditions(Arc::clone(&context)).await;
66        let sched_handle = {
67            let cancel_clone = cancel_task.clone();
68            self.base.get_schedule_handle(Arc::clone(&context), cancel_clone, comms_end, None).await
69        };
70        tokio::pin!(sched_handle);
71        let safe_mon = context.super_v().safe_mon();
72        tokio::select!(
73            _ = &mut sched_handle => {
74                context.k().con().send_tasklist().await;
75            },
76            () = safe_mon.notified() => {
77                cancel_task.cancel();
78                sched_handle.await.ok();
79
80                // Return to mapping mode
81                return OpExitSignal::ReInit(Box::new(self.clone()))
82            }
83        );
84        OpExitSignal::Continue
85    }
86
87    /// Delegates to the default orbital task wait logic, including early exits.
88    ///
89    /// # Arguments
90    /// * `c` – The mode context.
91    /// * `due` – Scheduled time of task execution.
92    ///
93    /// # Returns
94    /// * `WaitExitSignal` – Signal indicating why the wait was interrupted (if at all).
95    async fn exec_task_wait(&self, c: Arc<ModeContext>, due: DateTime<Utc>) -> WaitExitSignal {
96        <Self as OrbitalMode>::exec_task_wait(self, c, due).await
97    }
98
99    /// Executes a single scheduled task.
100    ///
101    /// Only state-switching tasks are valid in this mode. Other task types will cause a fatal error.
102    ///
103    /// # Arguments
104    /// * `context` – Mode context for task execution.
105    /// * `task` – Task to execute.
106    ///
107    /// # Returns
108    /// * `ExecExitSignal::Continue` – Always returned unless fatal occurs.
109    async fn exec_task(&self, context: Arc<ModeContext>, task: Task) -> ExecExitSignal {
110        match task.task_type() {
111            BaseTask::SwitchState(switch) => self.base.get_task(context, *switch).await,
112            _ => {
113                fatal!(
114                    "Illegal task type {} for state {}!",
115                    task.task_type(),
116                    Self::MODE_NAME
117                )
118            }
119        }
120        ExecExitSignal::Continue
121    }
122
123    /// Handles the transition into `FlightState::Safe`, executing a fallback escape sequence
124    /// and finishing the orbit phase.
125    ///
126    /// # Arguments
127    /// * `context` – Mode context for safe handling.
128    ///
129    /// # Returns
130    /// * `OpExitSignal::ReInit` – Always reinitializes the current mode.
131    async fn safe_handler(&self, context: Arc<ModeContext>) -> OpExitSignal {
132        FlightComputer::escape_safe(context.k().f_cont(), false).await;
133        context.o_ch_lock().write().await.finish(
134            context.k().f_cont().read().await.current_pos(),
135            self.safe_mode_rationale(),
136        );
137        OpExitSignal::ReInit(Box::new(self.clone()))
138    }
139
140    /// Handles the detection of a new Zoned Objective.
141    ///
142    /// Attempts to switch to a `ZOPrepMode`. If the objective is unreachable, logs a warning and continues.
143    ///
144    /// # Arguments
145    /// * `c` – Shared context.
146    /// * `obj` – The newly received zoned objective.
147    ///
148    /// # Returns
149    /// * `Some(OpExitSignal::ReInit)` – If transition to `ZOPrepMode` is feasible.
150    /// * `None` – If the objective is not reachable (e.g., burn not possible).
151    async fn zo_handler(&self, c: &Arc<ModeContext>, obj: KnownImgObjective) -> OptOpExitSignal {
152        let id = obj.id();
153        obj!("Found new Zoned Objective {id}!");
154
155        if let Some(zo_mode) = ZOPrepMode::from_obj(c, obj, self.base).await {
156            c.o_ch_lock().write().await.finish(
157                c.k().f_cont().read().await.current_pos(),
158                self.new_zo_rationale(),
159            );
160            Some(OpExitSignal::ReInit(Box::new(zo_mode)))
161        } else {
162            warn!("Skipping Objective, burn not feasible.");
163            None
164        }
165    }
166
167    /// Handles a beacon objective event by toggling to the complementary base mode.
168    /// Logs the beacon transition reason and returns a reinit signal to the new mode.
169    ///
170    /// # Arguments
171    /// * `context` – Shared mode context.
172    ///
173    /// # Returns
174    /// * `Some(OpExitSignal::ReInit)` – Always switches to the next beacon mode.
175    async fn bo_event_handler(&self, context: &Arc<ModeContext>) -> OptOpExitSignal {
176        let base = self.base.bo_event();
177        self.log_bo_event(context, base).await;
178        Some(OpExitSignal::ReInit(Box::new(Self { base })))
179    }
180
181    /// Performs final cleanup when exiting the mode and marks the phase as finished.
182    ///
183    /// # Arguments
184    /// * `context` – Shared context.
185    ///
186    /// # Returns
187    /// * `Box<dyn GlobalMode>` – A boxed copy of the current mode.
188    async fn exit_mode(&self, context: Arc<ModeContext>) -> Box<dyn GlobalMode> {
189        context.o_ch_lock().write().await.finish(
190            context.k().f_cont().read().await.current_pos(),
191            self.tasks_done_rationale(),
192        );
193        Box::new(self.clone())
194    }
195}