melvin_ob/mode_control/mode/
zo_prep_mode.rs1use super::{
2 global_mode::{GlobalMode, OrbitalMode},
3 in_orbit_mode::InOrbitMode,
4 zo_retrieval_mode::ZORetrievalMode,
5};
6use crate::flight_control::{
7 FlightComputer, FlightState,
8 orbit::{BurnSequence, ExitBurnResult},
9};
10use crate::objective::KnownImgObjective;
11use crate::scheduling::{
12 EndCondition, TaskController,
13 task::{BaseTask, Task},
14};
15use crate::util::logger::JsonDump;
16use crate::mode_control::{
17 base_mode::BaseMode,
18 mode_context::ModeContext,
19 signal::{ExecExitSignal, OpExitSignal, OptOpExitSignal, WaitExitSignal},
20};
21use crate::{error, fatal, info, log, log_burn, obj};
22use async_trait::async_trait;
23use chrono::{DateTime, TimeDelta, Utc};
24use std::{
25 mem::discriminant,
26 sync::{
27 Arc,
28 atomic::{AtomicBool, Ordering},
29 },
30};
31use tokio_util::sync::CancellationToken;
32
33pub(super) struct ZOPrepMode {
40 base: BaseMode,
42 exit_burn: ExitBurnResult,
44 target: KnownImgObjective,
46 left_orbit: AtomicBool,
48}
49
50impl Clone for ZOPrepMode {
51 fn clone(&self) -> Self {
52 Self {
53 base: self.base,
54 exit_burn: self.exit_burn.clone(),
55 target: self.target.clone(),
56 left_orbit: AtomicBool::new(self.left_orbit.load(Ordering::Acquire)),
57 }
58 }
59}
60
61impl ZOPrepMode {
62 const MODE_NAME: &'static str = "ZOPrepMode";
64 const MIN_REPLANNING_DT: TimeDelta = TimeDelta::seconds(500);
66
67 #[allow(clippy::cast_possible_wrap)]
78 pub(super) async fn from_obj(
79 context: &Arc<ModeContext>,
80 zo: KnownImgObjective,
81 curr_base: BaseMode,
82 ) -> Option<Self> {
83 log!("Trying ZOPrepMode for Zoned Objective: {}", zo.id());
84 let due = zo.end();
85 let (current_vel, fuel_left) = {
86 let f_cont_lock = context.k().f_cont();
87 let f_cont = f_cont_lock.read().await;
88 (f_cont.current_vel(), f_cont.fuel_left())
89 };
90 let start = zo.start();
91 if start > Utc::now() {
92 log!(
93 "Objective {} will be calculated as a short objective.",
94 zo.id()
95 );
96 }
97 let exit_burn = if zo.min_images() == 1 {
98 let target = zo.get_single_image_point();
99 TaskController::calculate_single_target_burn_sequence(
100 context.o_ch_clone().await.i_entry(),
101 current_vel,
102 target,
103 start,
104 due,
105 fuel_left,
106 zo.id(),
107 )
108 } else {
109 let entries = zo.get_corners();
110 TaskController::calculate_multi_target_burn_sequence(
111 context.o_ch_clone().await.i_entry(),
112 current_vel,
113 entries,
114 start,
115 due,
116 fuel_left,
117 zo.id(),
118 )
119 }?;
120 Self::log_burn(&exit_burn, &zo);
121 let base = Self::overthink_base(context, curr_base, exit_burn.sequence()).await;
122 exit_burn.dump_json();
123 Some(ZOPrepMode { base, exit_burn, target: zo, left_orbit: AtomicBool::new(false) })
124 }
125
126 fn log_burn(exit_burn: &ExitBurnResult, target: &KnownImgObjective) {
132 let exit_burn_seq = exit_burn.sequence();
133 let entry_pos = exit_burn_seq.sequence_pos().first().unwrap();
134 let exit_pos = exit_burn_seq.sequence_pos().last().unwrap();
135 let entry_t = exit_burn_seq.start_i().t().format("%H:%M:%S").to_string();
136 let vel = exit_burn_seq.sequence_vel().last().unwrap();
137 let tar = exit_burn.target_pos();
138 let add_tar = exit_burn.add_target();
139 let det_dt = exit_burn_seq.detumble_dt();
140 let acq_dt = exit_burn_seq.acc_dt();
141 let tar_unwrap = exit_burn.unwrapped_target();
142 info!(
143 "Calculated Burn Sequence for Zoned Objective: {}",
144 target.id()
145 );
146 log_burn!("Entry at {entry_t}, Position will be {entry_pos}");
147 log_burn!("Exit after {acq_dt}s, Position will be {exit_pos}. Detumble time is {det_dt}s.");
148 log_burn!(
149 "Exit Velocity will be {vel} aiming for target at {tar} unwrapped to {tar_unwrap}."
150 );
151 if let Some(tar2) = add_tar {
152 log_burn!("Additional Target will be {tar2}");
153 }
154 }
155
156 fn new_base(&self, base: BaseMode) -> Self {
164 Self {
165 base,
166 exit_burn: self.exit_burn.clone(),
167 target: self.target.clone(),
168 left_orbit: AtomicBool::new(self.left_orbit.load(Ordering::Acquire)),
169 }
170 }
171
172 #[allow(clippy::cast_possible_wrap)]
183 async fn overthink_base(c: &Arc<ModeContext>, base: BaseMode, burn: &BurnSequence) -> BaseMode {
184 if matches!(base, BaseMode::MappingMode) {
185 return BaseMode::MappingMode;
186 }
187 let burn_start = burn.start_i().t();
188 let worst_case_first_comms_end = {
189 let to_dt = FlightComputer::get_to_comms_t_est(c.k().f_cont()).await;
190 let state_change = FlightState::Comms.td_dt_to(FlightState::Acquisition);
191 to_dt + TimeDelta::seconds(TaskController::IN_COMMS_SCHED_SECS as i64) + state_change
192 };
193 if worst_case_first_comms_end + TimeDelta::seconds(5) > burn_start {
194 let t = worst_case_first_comms_end.format("%d %H:%M:%S").to_string();
195 log!("Requested BOScanningMode not feasible, first comms end is {t}.");
196 BaseMode::MappingMode
197 } else {
198 BaseMode::BeaconObjectiveScanningMode
199 }
200 }
201}
202
203impl OrbitalMode for ZOPrepMode {
204 fn base(&self) -> &BaseMode { &self.base }
206}
207
208#[async_trait]
209impl GlobalMode for ZOPrepMode {
210 fn type_name(&self) -> &'static str { Self::MODE_NAME }
212
213 async fn init_mode(&self, context: Arc<ModeContext>) -> OpExitSignal {
224 let cancel_task = CancellationToken::new();
225 let new_base = Self::overthink_base(&context, self.base, self.exit_burn.sequence()).await;
226 if discriminant(&self.base) != discriminant(&new_base) {
227 return OpExitSignal::ReInit(Box::new(self.new_base(new_base)));
228 }
229 let comms_end = self.base.handle_sched_preconditions(Arc::clone(&context)).await;
230 let end = EndCondition::from_burn(self.exit_burn.sequence());
231 let sched_handle = {
232 let cancel_clone = cancel_task.clone();
233 self.base
234 .get_schedule_handle(Arc::clone(&context), cancel_clone, comms_end, Some(end))
235 .await
236 };
237 tokio::pin!(sched_handle);
238 let safe_mon = context.super_v().safe_mon();
239 tokio::select!(
240 _ = &mut sched_handle => {
241 info!("Additionally scheduling Orbit Escape Burn Sequence!");
242 context.k().t_cont().schedule_vel_change(self.exit_burn.sequence().clone()).await;
243 context.k().con().send_tasklist().await;
244 },
245 () = safe_mon.notified() => {
246 cancel_task.cancel();
247 sched_handle.await.ok();
248 return self.safe_handler(context).await;
249 }
250 );
251 OpExitSignal::Continue
252 }
253
254 async fn exec_task_wait(&self, c: Arc<ModeContext>, due: DateTime<Utc>) -> WaitExitSignal {
256 <Self as OrbitalMode>::exec_task_wait(self, c, due).await
257 }
258
259 async fn exec_task(&self, context: Arc<ModeContext>, task: Task) -> ExecExitSignal {
268 match task.task_type() {
269 BaseTask::SwitchState(switch) => self.base.get_task(context, *switch).await,
270 BaseTask::ChangeVelocity(vel_change) => {
271 let pos = context.k().f_cont().read().await.current_pos();
272 log_burn!(
273 "Burn started at Pos {pos}. Expected Position was: {}.",
274 vel_change.burn().sequence_pos()[0]
275 );
276 FlightComputer::execute_burn(context.k().f_cont(), vel_change.burn()).await;
277 self.left_orbit.store(true, Ordering::Release);
278 }
279 BaseTask::TakeImage(_) => fatal!(
280 "Illegal task type {} for state {}!",
281 task.task_type(),
282 Self::MODE_NAME
283 ),
284 }
285 ExecExitSignal::Continue
286 }
287
288 async fn safe_handler(&self, context: Arc<ModeContext>) -> OpExitSignal {
290 FlightComputer::escape_safe(context.k().f_cont(), false).await;
291 context.o_ch_lock().write().await.finish(
292 context.k().f_cont().read().await.current_pos(),
293 self.safe_mode_rationale(),
294 );
295 let new = Self::from_obj(&context, self.target.clone(), self.base).await;
296 OpExitSignal::ReInit(new.map_or(Box::new(InOrbitMode::new(self.base)), |b| Box::new(b)))
297 }
298
299 async fn zo_handler(&self, c: &Arc<ModeContext>, obj: KnownImgObjective) -> OptOpExitSignal {
310 let burn_dt_cond =
311 self.exit_burn.sequence().start_i().t() - Utc::now() > Self::MIN_REPLANNING_DT;
312 if obj.end() < self.target.end() && burn_dt_cond {
313 let new_obj_mode = Self::from_obj(c, obj.clone(), self.base).await;
314 if let Some(prep_mode) = new_obj_mode {
315 c.o_ch_lock().write().await.finish(
316 c.k().f_cont().read().await.current_pos(),
317 self.new_zo_rationale(),
318 );
319 obj!(
320 "Objective {} is prioritized. Stashing current ZO {}!",
321 obj.id(),
322 self.target.id()
323 );
324 c.k_buffer().lock().await.push(self.target.clone());
325 return Some(OpExitSignal::ReInit(Box::new(prep_mode)));
326 }
327 }
328 obj!("Objective {} is not prioritized. Stashing!", obj.id());
329 c.k_buffer().lock().await.push(obj);
330 None
331 }
332
333 async fn bo_event_handler(&self, context: &Arc<ModeContext>) -> OptOpExitSignal {
342 let prop_new_base = self.base.bo_event();
343 let new_base =
344 Self::overthink_base(context, prop_new_base, self.exit_burn.sequence()).await;
345 if discriminant(&self.base) == discriminant(&new_base) {
346 None
347 } else {
348 self.log_bo_event(context, new_base).await;
349 log!(
350 "Trying to change base mode from {} to {} due to BO Event!",
351 self.base,
352 new_base
353 );
354 Some(OpExitSignal::ReInit(Box::new(self.new_base(new_base))))
355 }
356 }
357
358 async fn exit_mode(&self, context: Arc<ModeContext>) -> Box<dyn GlobalMode> {
366 context.o_ch_lock().write().await.finish(
367 context.k().f_cont().read().await.current_pos(),
368 self.tasks_done_exit_rationale(),
369 );
370 if self.left_orbit.load(Ordering::Acquire) {
371 Box::new(ZORetrievalMode::new(
372 self.target.clone(),
373 self.exit_burn.add_target(),
374 *self.exit_burn.unwrapped_target(),
375 ))
376 } else {
377 error!("ZOPrepMode::exit_mode called without left_orbit flag set!");
378 Box::new(InOrbitMode::new(self.base))
379 }
380 }
381}