melvin_ob/flight_control/
flight_computer.rs

1use super::{
2    flight_state::FlightState,
3    orbit::{BurnSequence, ClosedOrbit, IndexedOrbitPosition},
4};
5use crate::http_handler::{
6    http_client,
7    http_request::{
8        control_put::ControlSatelliteRequest,
9        observation_get::ObservationRequest,
10        request_common::{JSONBodyHTTPRequestType, NoBodyHTTPRequestType},
11        reset_get::ResetRequest,
12    },
13};
14use crate::imaging::CameraAngle;
15use crate::util::{Vec2D, WrapDirection, helpers::MAX_DEC};
16use crate::{STATIC_ORBIT_VEL, error, fatal, info, log, log_burn, warn};
17use crate::scheduling::TaskController;
18use chrono::{DateTime, TimeDelta, Utc};
19use fixed::types::{I32F32, I64F64};
20use num::{ToPrimitive, Zero};
21use rand::Rng;
22use std::{
23    sync::Arc,
24    time::{Duration, Instant},
25};
26use tokio::sync::RwLock;
27
28pub type TurnsClockCClockTup = (
29    Vec<(Vec2D<I32F32>, Vec2D<I32F32>)>,
30    Vec<(Vec2D<I32F32>, Vec2D<I32F32>)>,
31);
32
33/// Represents the core flight computer for satellite control.
34/// It manages operations such as state changes, velocity updates,
35/// battery charging.
36///
37/// This system interfaces with an external HTTP-based control system to
38/// send requests for managing the satellite’s behavior (e.g., velocity updates,
39/// state transitions). Additionally, the file schedules satellite-related
40/// activities like image captures and ensures the system stays updated
41/// using observations.
42///
43/// Key methods allow high-level control, including state transitions, camera angle
44/// adjustments, and battery-related tasks.
45#[derive(Debug)]
46pub struct FlightComputer {
47    /// Current position of the satellite in 2D space.
48    current_pos: Vec2D<I32F32>,
49    /// Current velocity of the satellite in 2D space.
50    current_vel: Vec2D<I32F32>,
51    /// Current state of the satellite based on `FlightState`.
52    current_state: FlightState,
53    /// Target State if `current_state` is `FlightState::Transition`
54    target_state: Option<FlightState>,
55    /// Current angle of the satellite's camera (e.g., Narrow, Normal, Wide).
56    current_angle: CameraAngle,
57    /// Current battery level of the satellite.
58    current_battery: I32F32,
59    /// Maximum battery capacity of the satellite.
60    max_battery: I32F32,
61    /// Remaining fuel level for the satellite operations.
62    fuel_left: I32F32,
63    /// Timestamp marking the last observation update from the satellite.
64    last_observation_timestamp: DateTime<Utc>,
65    /// HTTP client for sending requests for satellite operations.
66    request_client: Arc<http_client::HTTPClient>,
67}
68
69impl FlightComputer {
70    /// A constant I32F32 0.0 value for fuel and battery min values
71    pub const MIN_0: I32F32 = I32F32::ZERO;
72    /// A constant I32F32 100.0 value for fuel and battery max values
73    pub const MAX_100: I32F32 = I32F32::lit("100.0");
74    /// Constant acceleration in target velocity vector direction
75    pub const ACC_CONST: I32F32 = I32F32::lit("0.02");
76    /// Constant fuel consumption per accelerating second
77    pub const FUEL_CONST: I32F32 = I32F32::lit("0.03");
78    /// Maximum decimal places that are used in the observation endpoint for velocity
79    pub const VEL_BE_MAX_DECIMAL: u8 = MAX_DEC;
80    /// Constant timeout for the `wait_for_condition`-method
81    const DEF_COND_TO: u32 = 3000;
82    /// Constant timeout for the `wait_for_condition`-method
83    const DEF_COND_PI: u16 = 500;
84    /// Constant transition to SAFE sleep time for all states
85    const TO_SAFE_SLEEP: Duration = Duration::from_secs(60);
86    /// Maximum absolute vel change for orbit return
87    const MAX_OR_VEL_CHANGE_ABS: I32F32 = I32F32::lit("1.5");
88    /// Deviation at which `MAX_VEL_CHANGE_ABS` should occur
89    const MAX_OR_VEL_CHANGE_DEV: I32F32 = I32F32::lit("160");
90    /// Maximum acceleration time needed for orbit return maneuvers (this is 2*50s, as we
91    /// only change velocity by 1.0, and 10s for minor maneuvers)
92    const MAX_OR_ACQ_ACC_TIME: I32F32 = I32F32::lit("160");
93    /// Maximum time spend in acquisition between burns for orbit returns (this is the distance
94    /// travelled during acceleration/brake (2*25) which leaves a maximum of 110 at max speed according to `MAX_OR_VEL_CHANGE_DEV`)
95    const MAX_OR_ACQ_TIME: I32F32 = I32F32::lit("156");
96    /// Minimum battery used in decision-making for after safe transition
97    const AFTER_SAFE_MIN_BATT: I32F32 = I32F32::lit("50");
98    /// Minimum battery needed to exit safe mode
99    const EXIT_SAFE_MIN_BATT: I32F32 = I32F32::lit("10.0");
100    /// Maximum absolute break velocity change
101    const DEF_BRAKE_ABS: I32F32 = I32F32::lit("1.0");
102    /// Maximum burn time for detumbling
103    const MAX_DETUMBLE_DT: TimeDelta = TimeDelta::seconds(20);
104    /// Legal Target States for State Change
105    const LEGAL_TARGET_STATES: [FlightState; 3] = [
106        FlightState::Acquisition,
107        FlightState::Charge,
108        FlightState::Comms,
109    ];
110
111    /// Debug method used to emulate a safe mode event
112    #[cfg(debug_assertions)]
113    pub fn one_time_safe(&mut self) {
114        self.current_state = FlightState::Transition;
115        self.target_state = None;
116    }
117
118    /// Initializes a new `FlightComputer` instance.
119    ///
120    /// # Arguments
121    /// - `request_client`: A reference to the HTTP client for sending simulation requests.
122    ///
123    /// # Returns
124    /// A fully initialized `FlightComputer` with up-to-date field values.
125    pub async fn new(request_client: Arc<http_client::HTTPClient>) -> FlightComputer {
126        let mut return_controller = FlightComputer {
127            current_pos: Vec2D::new(I32F32::zero(), I32F32::zero()),
128            current_vel: Vec2D::new(I32F32::zero(), I32F32::zero()),
129            current_state: FlightState::Deployment,
130            target_state: None,
131            current_angle: CameraAngle::Normal,
132            current_battery: I32F32::zero(),
133            max_battery: I32F32::zero(),
134            fuel_left: I32F32::zero(),
135            last_observation_timestamp: Utc::now(),
136            request_client,
137        };
138        return_controller.update_observation().await;
139        if return_controller.current_state == FlightState::Transition {
140            return_controller.target_state = Some(FlightState::Transition);
141        }
142        return_controller
143    }
144
145    /// Truncates the velocity components to a fixed number of decimal places, as defined by `VEL_BE_MAX_DECIMAL`,
146    /// and calculates the remainder (deviation) after truncation.
147    ///
148    /// # Arguments
149    /// - `vel`: A `Vec2D<I32F32>` representing the velocity to be truncated.
150    ///
151    /// # Returns
152    /// A tuple containing:
153    /// - A `Vec2D<I32F32>` with truncated velocity components.
154    /// - A `Vec2D<I64F64>` representing the fractional deviation from the truncation.
155    pub fn trunc_vel(vel: Vec2D<I32F32>) -> (Vec2D<I32F32>, Vec2D<I64F64>) {
156        let factor = I32F32::from_num(10f32.powi(i32::from(Self::VEL_BE_MAX_DECIMAL)));
157        let factor_f64 = I64F64::from_num(10f64.powi(i32::from(Self::VEL_BE_MAX_DECIMAL)));
158        let trunc_x = (vel.x() * factor).floor() / factor;
159        let trunc_y = (vel.y() * factor).floor() / factor;
160        let dev_x = (I64F64::from_num(vel.x()) * factor_f64).frac() / factor_f64;
161        let dev_y = (I64F64::from_num(vel.y()) * factor_f64).frac() / factor_f64;
162        (Vec2D::new(trunc_x, trunc_y), Vec2D::new(dev_x, dev_y))
163    }
164
165    /// Rounds velocity by multiplying with `10^Self::VEL_BE_MAX_DECIMAL` and rounding afterward
166    ///
167    /// # Arguments
168    /// * vel: A `Vec2D<I32F32>` representing the velocity to be rounded
169    ///
170    /// # Returns
171    /// * A `Vec2D<I32F32>` representing the rounded, expanded velocity
172    pub fn round_vel_expand(vel: Vec2D<I32F32>) -> Vec2D<I32F32> {
173        let factor = I32F32::from_num(10f32.powi(i32::from(Self::VEL_BE_MAX_DECIMAL)));
174        let trunc_x = (vel.x() * factor).round();
175        let trunc_y = (vel.y() * factor).round();
176        Vec2D::new(trunc_x, trunc_y)
177    }
178
179    /// Rounds velocity by multiplying with `10^Self::VEL_BE_MAX_DECIMAL`, rounding and dividing back.
180    ///
181    /// # Arguments
182    /// * vel: A `Vec2D<I32F32>` representing the velocity to be rounded
183    ///
184    /// # Returns
185    /// * A `Vec2D<I32F32>` representing the rounded velocity
186    pub fn round_vel(vel: Vec2D<I32F32>) -> (Vec2D<I32F32>, Vec2D<I64F64>) {
187        let factor = I32F32::from_num(10f32.powi(i32::from(Self::VEL_BE_MAX_DECIMAL)));
188        let factor_f64 = I64F64::from_num(10f64.powi(i32::from(Self::VEL_BE_MAX_DECIMAL)));
189        let trunc_x = (vel.x() * factor).round() / factor;
190        let trunc_y = (vel.y() * factor).round() / factor;
191        let dev_x = (I64F64::from_num(vel.x()) * factor_f64).frac() / factor_f64;
192        let dev_y = (I64F64::from_num(vel.y()) * factor_f64).frac() / factor_f64;
193        (Vec2D::new(trunc_x, trunc_y), Vec2D::new(dev_x, dev_y))
194    }
195
196    /// Precomputes possible turns of MELVIN, splitting paths into clockwise and counterclockwise
197    /// directions based on the initial velocity. These precomputed paths are useful for calculating
198    /// optimal burns.
199    ///
200    /// # Arguments
201    /// - `init_vel`: A `Vec2D<I32F32>` representing the initial velocity of the satellite.
202    ///
203    /// # Returns
204    /// A tuple of vectors containing possible turns:
205    /// - The first vector contains possible clockwise turns `(position, velocity)`.
206    /// - The second vector contains possible counterclockwise turns `(position, velocity)`.
207    #[allow(clippy::cast_possible_truncation)]
208    pub fn compute_possible_turns(init_vel: Vec2D<I32F32>) -> TurnsClockCClockTup {
209        let start_x = init_vel.x();
210        let end_x = I32F32::zero();
211        let start_y = init_vel.y();
212        let end_y = I32F32::zero();
213
214        let step_x =
215            if start_x > end_x { -FlightComputer::ACC_CONST } else { FlightComputer::ACC_CONST };
216        let step_y =
217            if start_y > end_y { -FlightComputer::ACC_CONST } else { FlightComputer::ACC_CONST };
218
219        // Calculates changes along the X-axis while keeping the Y-axis constant.
220        let y_const_x_change: Vec<(Vec2D<I32F32>, Vec2D<I32F32>)> = {
221            let mut x_pos_vel = Vec::new();
222            let step = Vec2D::new(step_x, I32F32::zero());
223            let i_last = (start_x / step_x).ceil().abs().to_num::<i32>();
224            let mut next_pos = Vec2D::new(I32F32::zero(), I32F32::zero());
225            let mut next_vel = init_vel + step;
226            x_pos_vel.push((next_pos.round(), next_vel.round_to_2()));
227            for i in 0..i_last {
228                next_pos = next_pos + next_vel;
229                if i == i_last - 1 {
230                    next_vel = Vec2D::new(I32F32::zero(), start_y);
231                } else {
232                    next_vel = next_vel + step;
233                }
234                x_pos_vel.push((next_pos.round(), next_vel.round_to_2()));
235            }
236            x_pos_vel
237        };
238
239        // Calculates changes along the Y-axis while keeping the X-axis constant.
240        let x_const_y_change: Vec<(Vec2D<I32F32>, Vec2D<I32F32>)> = {
241            let mut y_pos_vel = Vec::new();
242            let step = Vec2D::new(I32F32::zero(), step_y);
243            let i_last = (start_y / step_y).ceil().abs().to_num::<i32>();
244            let mut next_pos = Vec2D::new(I32F32::zero(), I32F32::zero());
245            let mut next_vel = init_vel + step;
246            y_pos_vel.push((next_pos.round(), next_vel.round_to_2()));
247            for i in 0..i_last {
248                next_pos = next_pos + next_vel;
249                if i == i_last - 1 {
250                    next_vel = Vec2D::new(start_x, I32F32::zero());
251                } else {
252                    next_vel = next_vel + step;
253                }
254                y_pos_vel.push((next_pos.round(), next_vel.round_to_2()));
255            }
256            y_pos_vel
257        };
258
259        // Determine the ordering of clockwise and counterclockwise turns based on the direction of steps.
260        if step_x.signum() == step_y.signum() {
261            (y_const_x_change, x_const_y_change)
262        } else {
263            (x_const_y_change, y_const_x_change)
264        }
265    }
266
267    /// Retrieves the current position of the satellite.
268    ///
269    /// # Returns
270    /// A `Vec2D` representing the current satellite position.
271    pub fn current_pos(&self) -> Vec2D<I32F32> { self.current_pos }
272
273    /// Retrieves the current position of the satellite.
274    ///
275    /// # Returns
276    /// A `Vec2D` representing the current satellite position.
277    pub fn current_angle(&self) -> CameraAngle { self.current_angle }
278
279    /// Retrieves the current position of the velocity.
280    ///
281    /// # Returns
282    /// A `Vec2D` representing the current satellite velocity.
283    pub fn current_vel(&self) -> Vec2D<I32F32> { self.current_vel }
284
285    /// Retrieves the maximum battery capacity of the satellite.
286    ///
287    /// This value fluctuates only due to battery depletion safe mode events.
288    ///
289    /// # Returns
290    /// - A `I32F32` value representing the maximum battery charge.
291    pub fn max_battery(&self) -> I32F32 { self.max_battery }
292
293    /// Retrieves the current battery charge level of the satellite.
294    ///
295    /// # Returns
296    /// - A `I32F32` value denoting the battery's current charge level.
297    pub fn current_battery(&self) -> I32F32 { self.current_battery }
298
299    /// Retrieves the remaining fuel level of the satellite.
300    ///
301    /// # Returns
302    /// - A `I32F32` value representing the remaining percentage of fuel.
303    pub fn fuel_left(&self) -> I32F32 { self.fuel_left }
304
305    /// Retrieves the current operational state of the satellite.
306    ///
307    /// The state of the satellite determines its behavior, such as charging (`Charge`),
308    /// image acquisition (`Acquisition`), ...
309    ///
310    /// # Returns
311    /// - A `FlightState` enum denoting the active operational state.
312    pub fn state(&self) -> FlightState { self.current_state }
313
314    /// Retrieves the current target state of the satellite.
315    ///
316    /// The target state represents the resulting state of a commanded State change.
317    ///
318    /// # Returns
319    /// - A `Option<FlightState>` denoting the target state of the commanded state change.
320    pub fn target_state(&self) -> Option<FlightState> { self.target_state }
321
322    /// Retrieves a clone of the HTTP client used by the flight computer for sending requests.
323    ///
324    /// # Returns
325    /// - An `Arc<http_client::HTTPClient>` which represents the HTTP client instance.
326    pub fn client(&self) -> Arc<http_client::HTTPClient> { Arc::clone(&self.request_client) }
327
328    /// Sends a reset request to the satellite's HTTP control system.
329    ///
330    /// This function invokes an HTTP request to reset MELVIN, and it expects the request to succeed.
331    ///
332    /// # Panics
333    /// - If the reset request fails, this method will panic with an error message.
334    pub async fn reset(&mut self) {
335        ResetRequest {}
336            .send_request(&self.request_client)
337            .await
338            .unwrap_or_else(|_| fatal!("Failed to reset"));
339        Self::wait_for_duration(Duration::from_secs(4), false).await;
340        self.target_state = None;
341        log!("Reset request complete.");
342    }
343
344    /// Indicates that a `Supervisor` detected a safe mode event
345    pub fn safe_detected(&mut self) { self.target_state = Some(FlightState::Safe); }
346
347    /// Waits for a given amount of time with debug prints, this is a static method.
348    ///
349    /// # Arguments
350    /// - `sleep`: The duration for which the system should wait.
351    pub async fn wait_for_duration(sleep: Duration, mute: bool) {
352        if sleep.as_secs() == 0 {
353            if !mute {
354                log!("Wait call rejected! Duration was 0!");
355            };
356            return;
357        }
358        if !mute {
359            info!("Waiting for {} seconds!", sleep.as_secs());
360        };
361        tokio::time::sleep(sleep).await;
362    }
363
364    /// Waits for a condition to be met within a specified timeout.
365    ///
366    /// # Arguments
367    /// - `self_lock`: A `RwLock<Self>` reference to the active flight computer.
368    /// - `condition`: A closure that takes a reference to `Self` and returns a `bool`.
369    /// - `rationale`: A string describing the purpose of the wait.
370    /// - `timeout_millis`: Maximum time in milliseconds to wait for the condition to be met.
371    /// - `poll_interval`: Interval in milliseconds to check the condition.
372    ///
373    /// # Behavior
374    /// - The method continuously polls the condition at the specified interval until:
375    ///   - The condition returns `true`, or
376    ///   - The timeout expires.
377    /// - Logs the rationale and results of the wait.
378    async fn wait_for_condition<F>(
379        self_lock: &RwLock<Self>,
380        (condition, rationale): (F, String),
381        timeout_millis: u32,
382        poll_interval: u16,
383        mute: bool,
384    ) where
385        F: Fn(&Self) -> bool,
386    {
387        if !mute {
388            log!("Waiting for condition: {rationale}");
389        }
390        let start_time = Instant::now();
391        while start_time.elapsed().as_millis() < u128::from(timeout_millis) {
392            let cond = { condition(&*self_lock.read().await) };
393            if cond {
394                if !mute {
395                    let dt = start_time.elapsed().as_millis();
396                    if dt > 1000 {
397                        log!("Condition met after {} s", dt / 1000);
398                    } else {
399                        log!("Condition met after {dt} ms");
400                    }
401                }
402                return;
403            }
404            tokio::time::sleep(Duration::from_millis(u64::from(poll_interval))).await;
405        }
406        if mute {
407            warn!("Condition: {rationale} not met after {timeout_millis:#?} ms");
408        } else {
409            warn!("Condition not met after {timeout_millis:#?} ms");
410        }
411    }
412
413    /// This method is used to escape a safe mode event by first waiting for the minimum charge
414    /// and then transitioning back to an operational state.
415    ///
416    /// # Arguments
417    /// * `self_lock`: A shared `RwLock` containing the `FlightComputer` instance
418    /// * `force_charge`: A variable indicating whether the `FlightState` after escaping should be forced to `FlightState::Charge`
419    pub async fn escape_safe(self_lock: Arc<RwLock<Self>>, force_charge: bool) {
420        let target_state = {
421            let init_batt = self_lock.read().await.current_battery();
422            if init_batt <= Self::AFTER_SAFE_MIN_BATT || force_charge {
423                FlightState::Charge
424            } else {
425                FlightState::Acquisition
426            }
427        };
428        let mut curr_state = self_lock.read().await.state();
429        info!("Safe Mode Runtime initiated. Transitioning back to {target_state} asap.");
430        if curr_state == FlightState::Transition {
431            Self::wait_for_duration(Self::TO_SAFE_SLEEP, false).await;
432            Self::avoid_transition(&self_lock).await;
433            curr_state = {
434                let mut lock = self_lock.write().await;
435                lock.target_state = None;
436                lock.current_state
437            };
438        }
439        if curr_state != FlightState::Safe {
440            error!("State is not safe but {}", curr_state);
441        }
442        let cond_min_charge = (
443            |cont: &FlightComputer| cont.current_battery() > Self::EXIT_SAFE_MIN_BATT,
444            format!("Battery level is higher than {}", Self::EXIT_SAFE_MIN_BATT),
445        );
446        Self::wait_for_condition(
447            &self_lock,
448            cond_min_charge,
449            450_000,
450            Self::DEF_COND_PI,
451            false,
452        )
453        .await;
454        Self::set_state_wait(self_lock, target_state).await;
455    }
456
457    /// A small helper method which waits for the current transition phase to end.
458    ///
459    /// # Arguments
460    /// * `self_lock`: A shared `RwLock` containing the `FlightComputer` instance
461    pub async fn avoid_transition(self_lock: &Arc<RwLock<Self>>) {
462        let not_trans = (
463            |cont: &FlightComputer| cont.state() != FlightState::Transition,
464            format!("State is not {}", FlightState::Transition),
465        );
466        let max_dt = FlightState::Safe.dt_to(FlightState::Acquisition);
467        Self::wait_for_condition(
468            self_lock,
469            not_trans,
470            u32::try_from(max_dt.as_millis()).unwrap_or(u32::MAX),
471            Self::DEF_COND_PI,
472            false,
473        )
474        .await;
475        self_lock.write().await.target_state = None;
476    }
477
478    /// A helper method which transitions state-aware to [`FlightState::Comms`].
479    ///
480    /// # Arguments
481    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
482    #[allow(clippy::cast_possible_wrap)]
483    pub async fn get_to_comms(self_lock: Arc<RwLock<Self>>) -> DateTime<Utc> {
484        if self_lock.read().await.state() == FlightState::Comms {
485            let batt_diff =
486                self_lock.read().await.current_battery() - TaskController::MIN_BATTERY_THRESHOLD;
487            let rem_t = (batt_diff / FlightState::Comms.get_charge_rate()).abs().ceil();
488            let add_t = TimeDelta::seconds(rem_t.to_num::<i64>()).min(TimeDelta::seconds(
489                TaskController::IN_COMMS_SCHED_SECS as i64,
490            ));
491            return Utc::now() + add_t;
492        }
493        let charge_dt = Self::get_charge_dt_comms(&self_lock).await;
494        log!("Charge time for comms: {}", charge_dt);
495
496        if charge_dt > 0 {
497            FlightComputer::set_state_wait(Arc::clone(&self_lock), FlightState::Charge).await;
498            FlightComputer::wait_for_duration(Duration::from_secs(charge_dt), false).await;
499            FlightComputer::set_state_wait(self_lock, FlightState::Comms).await;
500        } else {
501            FlightComputer::set_state_wait(self_lock, FlightState::Comms).await;
502        }
503        Utc::now() + TimeDelta::seconds(TaskController::IN_COMMS_SCHED_SECS as i64)
504    }
505
506    /// A helper method used to get out of [`FlightState::Comms`] and back to an operational [`FlightState`].
507    ///
508    /// # Arguments
509    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
510    #[allow(clippy::cast_possible_wrap)]
511    pub async fn escape_if_comms(self_lock: Arc<RwLock<Self>>) -> DateTime<Utc> {
512        let (state, batt) = {
513            let f_cont = self_lock.read().await;
514            (f_cont.state(), f_cont.current_battery())
515        };
516        if state == FlightState::Comms {
517            let half_batt =
518                (TaskController::MAX_BATTERY_THRESHOLD + TaskController::MIN_BATTERY_THRESHOLD) / 2;
519            if batt > half_batt {
520                FlightComputer::set_state_wait(Arc::clone(&self_lock), FlightState::Acquisition)
521                    .await;
522            } else {
523                FlightComputer::set_state_wait(Arc::clone(&self_lock), FlightState::Charge).await;
524            }
525        }
526        Utc::now()
527    }
528
529    /// A helper method estimating the `DateTime<Utc>` when a transition to [`FlightState::Comms`] could be finished.
530    ///
531    /// # Arguments
532    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
533    #[allow(clippy::cast_possible_wrap)]
534    pub async fn get_to_comms_t_est(self_lock: Arc<RwLock<Self>>) -> DateTime<Utc> {
535        let t_time = FlightState::Charge.td_dt_to(FlightState::Comms);
536        if self_lock.read().await.state() == FlightState::Comms {
537            let batt_diff =
538                self_lock.read().await.current_battery() - TaskController::MIN_BATTERY_THRESHOLD;
539            let rem_t = (batt_diff / FlightState::Comms.get_charge_rate().abs()).abs().ceil();
540            return Utc::now() + TimeDelta::seconds(rem_t.to_num::<i64>());
541        }
542        let charge_dt = Self::get_charge_dt_comms(&self_lock).await;
543
544        if charge_dt > 0 {
545            Utc::now() + TimeDelta::seconds(charge_dt as i64) + t_time * 2
546        } else {
547            Utc::now() + t_time
548        }
549    }
550
551    /// A helper method used to perform an acceleration maneuver to get to `STATIC_ORBIT_VEL`.
552    ///
553    /// # Arguments
554    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
555    pub async fn get_to_static_orbit_vel(self_lock: &Arc<RwLock<Self>>) {
556        let orbit_vel = Vec2D::from(STATIC_ORBIT_VEL);
557        let (batt, vel) = {
558            let f_cont = self_lock.read().await;
559            (f_cont.current_battery(), f_cont.current_vel())
560        };
561        if vel == orbit_vel {
562            return;
563        }
564        let vel_change_dt = Duration::from_secs_f32(
565            (orbit_vel.euclid_distance(&vel) / Self::ACC_CONST).to_num::<f32>(),
566        );
567        let charge_needed = {
568            let acq_acc_db =
569                FlightState::Acquisition.get_charge_rate() + FlightState::ACQ_ACC_ADDITION;
570            let or_vel_corr_db = I32F32::from_num(vel_change_dt.as_secs()) * acq_acc_db;
571            TaskController::MIN_BATTERY_THRESHOLD + or_vel_corr_db.abs()
572        };
573        log!("Getting back to orbit velocity {orbit_vel}. Minimum charge needed: {charge_needed}");
574        if batt < charge_needed {
575            FlightComputer::charge_full_wait(self_lock).await;
576        }
577        let state = self_lock.read().await.state();
578        if !matches!(state, FlightState::Acquisition) {
579            FlightComputer::set_state_wait(Arc::clone(self_lock), FlightState::Acquisition).await;
580        }
581        FlightComputer::set_vel_wait(Arc::clone(self_lock), orbit_vel, true).await;
582    }
583
584    /// A helper method calculating the charge difference for a transition to `FlightState::Comms`.
585    ///
586    /// # Arguments
587    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
588    ///
589    /// # Returns
590    /// A `u64` resembling the necessary number of charging seconds
591    async fn get_charge_dt_comms(self_lock: &Arc<RwLock<Self>>) -> u64 {
592        let batt_diff = (self_lock.read().await.current_battery()
593            - TaskController::MIN_COMMS_START_CHARGE)
594            .min(I32F32::zero());
595        (-batt_diff / FlightState::Charge.get_charge_rate()).ceil().to_num::<u64>()
596    }
597
598    /// A helper method used to charge to the maximum battery threshold.
599    ///
600    /// # Arguments
601    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
602    pub async fn charge_full_wait(self_lock: &Arc<RwLock<Self>>) {
603        let max_batt = self_lock.read().await.max_battery;
604        Self::charge_to_wait(self_lock, max_batt).await;
605    }
606
607    /// A helper method used to charge to a given threshold.
608    ///
609    /// # Arguments
610    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
611    /// * `target_batt`: An `I32F32` resembling the desired target battery level
612    pub async fn charge_to_wait(self_lock: &Arc<RwLock<Self>>, target_batt: I32F32) {
613        let (state, battery) = {
614            let f_cont = self_lock.read().await;
615            (f_cont.state(), f_cont.current_battery())
616        };
617        if battery >= target_batt {
618            return;
619        }
620        if state == FlightState::Safe {
621            FlightComputer::escape_safe(Arc::clone(self_lock), true).await;
622        } else {
623            FlightComputer::set_state_wait(Arc::clone(self_lock), FlightState::Charge).await;
624        }
625        let batt = self_lock.read().await.current_battery();
626        let dt = (target_batt - batt) / FlightState::Charge.get_charge_rate();
627        Self::wait_for_duration(Duration::from_secs(dt.to_num::<u64>()), false).await;
628    }
629
630    /// Transitions the satellite to a new operational state and waits for transition completion.
631    ///
632    /// # Arguments
633    /// - `self_lock`: A `RwLock<Self>` reference to the active flight computer.
634    /// - `new_state`: The target operational state.
635    pub async fn set_state_wait(self_lock: Arc<RwLock<Self>>, new_state: FlightState) {
636        let init_state = { self_lock.read().await.current_state };
637        if new_state == init_state {
638            log!("State already set to {new_state}");
639            return;
640        } else if !Self::LEGAL_TARGET_STATES.contains(&new_state) {
641            fatal!("State {new_state} is not a legal target state");
642        } else if init_state == FlightState::Transition {
643            fatal!(" State cant be changed when in {init_state}");
644        }
645        self_lock.write().await.target_state = Some(new_state);
646        self_lock.read().await.set_state(new_state).await;
647
648        let transition_t = init_state.dt_to(new_state);
649
650        Self::wait_for_duration(transition_t, false).await;
651        let cond = (
652            |cont: &FlightComputer| cont.state() == new_state,
653            format!("State equals {new_state}"),
654        );
655        Self::wait_for_condition(
656            &self_lock,
657            cond,
658            Self::DEF_COND_TO,
659            Self::DEF_COND_PI,
660            false,
661        )
662        .await;
663        self_lock.write().await.target_state = None;
664    }
665
666    /// Adjusts the velocity of the satellite and waits until the target velocity is reached.
667    ///
668    /// # Arguments
669    /// - `self_lock`: A `RwLock<Self>` reference to the active flight computer.
670    /// - `new_vel`: The target velocity vector.
671    pub async fn set_vel_wait(self_lock: Arc<RwLock<Self>>, new_vel: Vec2D<I32F32>, mute: bool) {
672        let (current_state, current_vel) = {
673            let f_cont_read = self_lock.read().await;
674            (f_cont_read.state(), f_cont_read.current_vel())
675        };
676        if current_state != FlightState::Acquisition {
677            fatal!("Velocity cant be changed in state {current_state}");
678        }
679        let vel_change_dt = Duration::from_secs_f32(
680            (new_vel.euclid_distance(&current_vel) / Self::ACC_CONST).to_num::<f32>(),
681        );
682        self_lock.read().await.set_vel(new_vel, mute).await;
683        if vel_change_dt.as_secs() > 0 {
684            Self::wait_for_duration(vel_change_dt, mute).await;
685        }
686        let comp_new_vel = Self::round_vel_expand(new_vel);
687        let cond = (
688            |cont: &FlightComputer| Self::round_vel_expand(cont.current_vel()) == comp_new_vel,
689            format!("Vel (Scaled) equals {new_vel}"),
690        );
691        Self::wait_for_condition(&self_lock, cond, Self::DEF_COND_TO, Self::DEF_COND_PI, mute)
692            .await;
693    }
694
695    /// Adjusts the satellite's camera angle and waits until the target angle is reached.
696    ///
697    /// # Arguments
698    /// - `self_lock`: A `RwLock<Self>` reference to the active flight computer.
699    /// - `new_angle`: The target camera angle.
700    ///
701    /// # Behavior
702    /// - If the current angle matches the new angle, logs the status and exits.
703    /// - Checks if the current state permits changing the camera angle.
704    ///   If not, it panics with a fatal error.
705    /// - Sets the new angle and waits until the system confirms it has been applied.
706    pub async fn set_angle_wait(self_lock: Arc<RwLock<Self>>, new_angle: CameraAngle) {
707        let (current_angle, current_state) = {
708            let f_cont_read = self_lock.read().await;
709            (f_cont_read.current_angle, f_cont_read.state())
710        };
711        if current_angle == new_angle {
712            log!("Angle already set to {new_angle}");
713            return;
714        }
715        if current_state != FlightState::Acquisition {
716            fatal!("Angle cant be changed in state {current_state}");
717        }
718
719        self_lock.read().await.set_angle(new_angle).await;
720        let cond = (
721            |cont: &FlightComputer| cont.current_angle() == new_angle,
722            format!("Lens equals {new_angle}"),
723        );
724        Self::wait_for_condition(
725            &self_lock,
726            cond,
727            Self::DEF_COND_TO,
728            Self::DEF_COND_PI,
729            false,
730        )
731        .await;
732    }
733
734    /// Executes a sequence of thruster burns that affect the trajectory of MELVIN.
735    ///
736    /// # Arguments
737    /// - `self_lock`: A `RwLock<Self>` reference to the active flight computer.
738    /// - `burn_sequence`: A reference to the sequence of executed thruster burns.
739    pub async fn execute_burn(self_lock: Arc<RwLock<Self>>, burn: &BurnSequence) {
740        let burn_start = Utc::now();
741        for vel_change in burn.sequence_vel() {
742            let st = tokio::time::Instant::now();
743            let dt = Duration::from_secs(1);
744            FlightComputer::set_vel_wait(Arc::clone(&self_lock), *vel_change, true).await;
745            let el = st.elapsed();
746            if el < dt {
747                tokio::time::sleep(dt).await;
748            }
749        }
750        let target_pos = burn.sequence_pos().last().unwrap();
751        let target_vel = burn.sequence_vel().last().unwrap();
752        let (pos, vel) = {
753            let f_cont = self_lock.read().await;
754            (f_cont.current_pos(), f_cont.current_vel())
755        };
756        let burn_dt = (Utc::now() - burn_start).num_seconds();
757        log_burn!(
758            "Burn sequence finished after {burn_dt}s! Position: {pos}, Velocity: {vel:.2}, expected Position: {target_pos:.0}, expected Velocity: {target_vel:.2}."
759        );
760    }
761
762    /// Executes an orbit return maneuver in a loop until the current position is recognized and assigned an orbit index.
763    ///
764    /// # Arguments
765    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
766    /// * `c_o`: A shared `RwLock` containing the [`ClosedOrbit`] instance
767    pub async fn or_maneuver(self_lock: Arc<RwLock<Self>>, c_o: Arc<RwLock<ClosedOrbit>>) -> usize {
768        if self_lock.read().await.state() != FlightState::Acquisition {
769            FlightComputer::set_state_wait(Arc::clone(&self_lock), FlightState::Acquisition).await;
770        }
771        let o_unlocked = c_o.read().await;
772        let (mut pos, vel) = {
773            let f_cont = self_lock.read().await;
774            (f_cont.current_pos(), f_cont.current_vel())
775        };
776        log!("Starting Orbit Return Deviation Compensation.");
777        let start = Utc::now();
778        while !o_unlocked.will_visit(pos) {
779            let (ax, dev) = o_unlocked.get_closest_deviation(pos);
780            let (dv, h_dt) = Self::compute_vmax_and_hold_time(dev);
781            log_burn!("Computed Orbit Return. Deviation on {ax} is {dev:.2} and vel is {vel:.2}.");
782            let corr_v = vel + Vec2D::from_axis_and_val(ax, dv);
783            log_burn!(
784                "Correction velocity is {corr_v:.2}, ramping by {dv:.2}. Hold time will be {h_dt}s."
785            );
786            FlightComputer::set_vel_wait(Arc::clone(&self_lock), corr_v, false).await;
787            if h_dt > 0 {
788                FlightComputer::wait_for_duration(Duration::from_secs(h_dt), false).await;
789            }
790            FlightComputer::set_vel_wait(Arc::clone(&self_lock), vel, false).await;
791            pos = self_lock.read().await.current_pos();
792        }
793        let dt = (Utc::now() - start).num_seconds();
794        let entry_i = o_unlocked.get_i(pos).unwrap();
795        info!("Orbit Return Deviation Compensation finished in {dt}s. New Orbit Index: {entry_i}");
796        entry_i
797    }
798
799    /// Helper method calculating the maximum charge needed for an orbit return maneuver.
800    ///
801    /// # Returns
802    /// * An `I32F32`, the maximum battery level
803    pub fn max_or_maneuver_charge() -> I32F32 {
804        let acq_db = FlightState::Acquisition.get_charge_rate();
805        let acq_acc_db = acq_db + FlightState::ACQ_ACC_ADDITION;
806        Self::MAX_OR_ACQ_ACC_TIME * acq_acc_db + Self::MAX_OR_ACQ_TIME * acq_db
807    }
808
809    /// Helper method computing the maximum orbit return maneuver velocity, trying either a triangular or trapezoidal profile.
810    ///
811    /// # Arguments
812    /// * `dev`: The absolute deviation on a singular axis as an `I32F32`
813    ///
814    /// # Returns
815    /// A tuple containing:
816    ///   - The maximum velocity change
817    ///   - The number of seconds to hold that velocity
818    fn compute_vmax_and_hold_time(dev: I32F32) -> (I32F32, u64) {
819        // Try triangular profile first (no cruising)
820        let dv_triang = dev.signum() * (Self::ACC_CONST * dev.abs()).sqrt();
821        if dv_triang.abs() <= Self::MAX_OR_VEL_CHANGE_ABS {
822            // Just accelerate to vmax_triangular and decelerate
823            (dv_triang, 0)
824        } else {
825            // Trapezoidal profile: accelerate to vmax_limit, hold, then decelerate
826            let t_ramp = Self::MAX_OR_VEL_CHANGE_ABS / Self::ACC_CONST;
827            let d_ramp = I32F32::from_num(0.5) * Self::MAX_OR_VEL_CHANGE_ABS * t_ramp; // distance per ramp
828            let d_hold = dev.abs() - 2 * d_ramp;
829            let t_hold = (d_hold / Self::MAX_OR_VEL_CHANGE_ABS).floor().to_num::<u64>();
830            (dev.signum() * Self::MAX_OR_VEL_CHANGE_ABS, t_hold)
831        }
832    }
833
834    /// A helper method used to stop an ongoing velocity change.
835    ///
836    /// # Arguments
837    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
838    pub async fn stop_ongoing_burn(self_lock: Arc<RwLock<Self>>) {
839        let (vel, state) = {
840            let f_cont = self_lock.read().await;
841            (f_cont.current_vel(), f_cont.state())
842        };
843        if state == FlightState::Acquisition {
844            FlightComputer::set_vel_wait(Arc::clone(&self_lock), vel, true).await;
845        }
846    }
847
848    /// Executes a sequence of velocity changes to accelerate towards a secondary target for a multi-target zoned objective.
849    ///
850    /// # Arguments
851    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
852    /// * `target`: The target position as a `Vec2D<I32F32>`
853    /// * `deadline`: The deadline as a `DateTime<Utc>`
854    pub async fn turn_for_2nd_target(
855        self_lock: Arc<RwLock<Self>>,
856        target: Vec2D<I32F32>,
857        deadline: DateTime<Utc>,
858    ) {
859        log!("Starting turn for second target");
860        let start = Utc::now();
861        let pos = self_lock.read().await.current_pos();
862        let mut last_to_target = pos.unwrapped_to(&target);
863        let ticker = 0;
864        loop {
865            let (pos, vel) = {
866                let f_cont = self_lock.read().await;
867                (f_cont.current_pos(), f_cont.current_vel())
868            };
869
870            let to_target = pos.unwrapped_to(&target);
871            let dt = to_target.abs() / vel.abs();
872            if !last_to_target.is_eq_signum(&to_target) {
873                let wait_dt = dt.to_num::<u64>()
874                    + TaskController::ZO_IMAGE_FIRST_DEL.num_seconds().to_u64().unwrap();
875                log!("Overshot target! Holding velocity change and waiting for 5s!");
876                FlightComputer::set_vel_wait(Arc::clone(&self_lock), vel, true).await;
877                FlightComputer::wait_for_duration(Duration::from_secs(wait_dt), false).await;
878                return;
879            }
880            last_to_target = to_target;
881            let dx = (pos + vel * dt).to(&target).round_to_2();
882            let new_vel = to_target.normalize() * vel.abs();
883
884            if ticker % 10 == 0 {
885                log_burn!("Turning: DX: {dx:.2}, direct DT: {dt:.2}s");
886            }
887            if dx.abs() < vel.abs() / 2 {
888                let turn_dt = (Utc::now() - start).num_seconds();
889                log!("Turning finished after {turn_dt}s with remaining DX: {dx:.2} and dt {dt:2}s");
890                FlightComputer::stop_ongoing_burn(Arc::clone(&self_lock)).await;
891                let sleep_dt = Duration::from_secs(dt.to_num::<u64>()) + Duration::from_secs(5);
892                tokio::time::sleep(sleep_dt).await;
893                return;
894            }
895            if Utc::now() > deadline {
896                let turn_dt = (Utc::now() - start).num_seconds();
897                log!("Turning timeout after {turn_dt}s with remaining DX: {dx:.2} and dt {dt:2}s");
898                FlightComputer::stop_ongoing_burn(Arc::clone(&self_lock)).await;
899            }
900            self_lock.write().await.set_vel(new_vel, true).await;
901            tokio::time::sleep(Duration::from_secs(1)).await;
902        }
903    }
904
905    /// Executes a sequence of velocity changes minimizing the deviation between an expected impact point and a target point.
906    ///
907    /// # Arguments
908    /// * `self_lock`: A shared `RwLock` containing the [`FlightComputer`] instance
909    /// * `target`: The target position as a `Vec2D<I32F32>`
910    /// * `lens`: The planned `CameraAngle` to derive the maximum absolute speed
911    ///
912    /// # Returns
913    /// A tuple containing:
914    ///   - A `DateTime<Utc>` when the target will be hit
915    ///   - A `Vec2D<I32F32>` containing the wrapped target position, if wrapping occured  
916    pub async fn detumble_to(
917        self_lock: Arc<RwLock<Self>>,
918        mut target: Vec2D<I32F32>,
919        lens: CameraAngle,
920    ) -> (DateTime<Utc>, Vec2D<I32F32>) {
921        let mut ticker: i32 = 0;
922        let max_speed = lens.get_max_speed();
923        let detumble_start = Utc::now();
924
925        let start_pos = self_lock.read().await.current_pos();
926        let mut to_target = start_pos.to(&target);
927        let mut dt;
928        let mut dx;
929        let mut last_to_target = to_target;
930        log!("Starting detumble to {target} (projected position).");
931        loop {
932            let (pos, vel) = {
933                let f_locked = self_lock.read().await;
934                (f_locked.current_pos(), f_locked.current_vel())
935            };
936            to_target = pos.to(&target);
937
938            // sudden jumps mean we wrapped and we need to wrap the corresponding coordinate too
939            if to_target.x().abs() > 2 * last_to_target.x().abs() {
940                target = target.wrap_by(&WrapDirection::WrapX);
941                to_target = pos.to(&target);
942            }
943            if to_target.y().abs() > 2 * last_to_target.y().abs() {
944                target = target.wrap_by(&WrapDirection::WrapY);
945                to_target = pos.to(&target);
946            }
947            last_to_target = to_target;
948            dt = (to_target.abs() / vel.abs()).round();
949            dx = (pos + vel * dt).to(&target).round_to_2();
950            let per_dx = dx.abs() / dt;
951
952            let acc = dx.normalize() * Self::ACC_CONST.min(per_dx * Self::rand_weight());
953            let mut new_vel = vel + FlightComputer::round_vel(acc).0;
954            let overspeed = new_vel.abs() > max_speed;
955            if overspeed {
956                let target_vel = new_vel.normalize() * (new_vel.abs() - Self::DEF_BRAKE_ABS);
957                let (trunc_vel, _) = FlightComputer::round_vel(target_vel);
958                new_vel = trunc_vel;
959            }
960            if ticker % 5 == 0 {
961                log_burn!("Detumbling Step {ticker}: DX: {dx:.2}, direct DT: {dt:2}s");
962                if overspeed {
963                    let overspeed_amount = vel.abs() - max_speed;
964                    warn!("Overspeeding by {overspeed_amount:.2}");
965                }
966            }
967            ticker += 1;
968            if dx.abs() < vel.abs() / 2 || Utc::now() - detumble_start > Self::MAX_DETUMBLE_DT {
969                let detumble_dt = (Utc::now() - detumble_start).num_seconds();
970                log!(
971                    "Detumbling finished after {detumble_dt}s with rem. DX: {dx:.2} and dt {dt:.2}s"
972                );
973                FlightComputer::stop_ongoing_burn(Arc::clone(&self_lock)).await;
974                FlightComputer::set_angle_wait(Arc::clone(&self_lock), lens).await;
975                return (Utc::now() + TimeDelta::seconds(dt.to_num::<i64>()), target);
976            }
977            if overspeed {
978                FlightComputer::set_vel_wait(Arc::clone(&self_lock), new_vel, true).await;
979            } else {
980                self_lock.write().await.set_vel(new_vel, true).await;
981            }
982            tokio::time::sleep(Duration::from_secs(1)).await;
983        }
984    }
985
986    /// Random weight to counter numeric local minima
987    ///
988    /// Returns
989    /// A `I32F32` representing a random weight in the range \[0.0, 10.0\]
990    fn rand_weight() -> I32F32 {
991        let mut rng = rand::rng();
992        I32F32::from_num(rng.random_range(0.0..10.0))
993    }
994
995    /// Updates the satellite's internal fields with the latest observation data.
996    ///
997    /// # Arguments
998    /// * A mutable reference to the `FlightComputer` instance
999    pub async fn update_observation(&mut self) {
1000        if let Ok(obs) = (ObservationRequest {}.send_request(&self.request_client).await) {
1001            self.current_pos =
1002                Vec2D::from((I32F32::from_num(obs.pos_x()), I32F32::from_num(obs.pos_y())));
1003            self.current_vel =
1004                Vec2D::from((I32F32::from_num(obs.vel_x()), I32F32::from_num(obs.vel_y())));
1005            self.current_state = FlightState::from(obs.state());
1006            self.current_angle = CameraAngle::from(obs.angle());
1007            self.last_observation_timestamp = obs.timestamp();
1008            self.current_battery =
1009                I32F32::from_num(obs.battery()).clamp(Self::MIN_0, Self::MAX_100);
1010            self.max_battery =
1011                I32F32::from_num(obs.max_battery()).clamp(Self::MIN_0, Self::MAX_100);
1012            self.fuel_left = I32F32::from_num(obs.fuel()).clamp(Self::MIN_0, Self::MAX_100);
1013        } else {
1014            error!("Unnoticed HTTP Error in updateObservation()");
1015        }
1016    }
1017
1018    /// Sets the satellite’s `FlightState`.
1019    ///
1020    /// # Arguments
1021    /// - `new_state`: The new operational state.
1022    async fn set_state(&self, new_state: FlightState) {
1023        let req = ControlSatelliteRequest {
1024            vel_x: self.current_vel.x().to_f64().unwrap(),
1025            vel_y: self.current_vel.y().to_f64().unwrap(),
1026            camera_angle: self.current_angle.into(),
1027            state: new_state.into(),
1028        };
1029        if req.send_request(&self.request_client).await.is_ok() {
1030            info!("State change started to {new_state}");
1031        } else {
1032            error!("Unnoticed HTTP Error in set_state()");
1033        }
1034    }
1035
1036    /// Sets the satellite’s velocity. The input velocity should only have two decimal places after comma.
1037    ///
1038    /// # Arguments
1039    /// - `new_vel`: The new velocity.
1040    async fn set_vel(&self, new_vel: Vec2D<I32F32>, mute: bool) {
1041        let (vel, _) = Self::round_vel(new_vel);
1042        let req = ControlSatelliteRequest {
1043            vel_x: vel.x().to_f64().unwrap(),
1044            vel_y: vel.y().to_f64().unwrap(),
1045            camera_angle: self.current_angle.into(),
1046            state: self.current_state.into(),
1047        };
1048
1049        if req.send_request(&self.request_client).await.is_ok() {
1050            if !mute {
1051                info!("Velocity change commanded to [{}, {}]", vel.x(), vel.y());
1052            }
1053        } else {
1054            error!("Unnoticed HTTP Error in set_state()");
1055        }
1056    }
1057
1058    /// Sets the satellite’s `CameraAngle`
1059    ///
1060    /// # Arguments
1061    /// - `new_angle`: The new Camera Angle.
1062    async fn set_angle(&self, new_angle: CameraAngle) {
1063        let req = ControlSatelliteRequest {
1064            vel_x: self.current_vel.x().to_f64().unwrap(),
1065            vel_y: self.current_vel.y().to_f64().unwrap(),
1066            camera_angle: new_angle.into(),
1067            state: self.current_state.into(),
1068        };
1069
1070        if req.send_request(&self.request_client).await.is_ok() {
1071            info!("Angle change commanded to {new_angle}");
1072        } else {
1073            error!("Unnoticed HTTP Error in set_state()");
1074        }
1075    }
1076
1077    /// Predicts the satellite’s position after a specified time interval.
1078    ///
1079    /// # Arguments
1080    /// - `time_delta`: The time interval for prediction.
1081    ///
1082    /// # Returns
1083    /// - A `Vec2D<I32F32>` representing the satellite’s predicted position.
1084    pub fn pos_in_dt(&self, now: IndexedOrbitPosition, dt: TimeDelta) -> IndexedOrbitPosition {
1085        let pos = self.current_pos
1086            + (self.current_vel * I32F32::from_num(dt.num_seconds())).wrap_around_map();
1087        let t = Utc::now() + dt;
1088        now.new_from_future_pos(pos, t)
1089    }
1090
1091    /// Helper method predicting the battery level after a specified time interval.
1092    ///
1093    /// # Arguments
1094    /// - `time_delta`: The time interval for prediction.
1095    ///
1096    /// # Returns
1097    /// - An `I32F32` representing the satellite’s predicted battery level
1098    pub fn batt_in_dt(&self, dt: TimeDelta) -> I32F32 {
1099        self.current_battery
1100            + (self.current_state.get_charge_rate() * I32F32::from_num(dt.num_seconds()))
1101    }
1102}