flight_computer/
flight_phase.rs

1//! # Flight phase
2//!
3//! This module deals with determining which phase of flight the device is in,
4//! as well as managing the altimeter.
5
6#[cfg(not(test))]
7use micromath::F32Ext;
8
9use crate::{
10    altimeter::Altimeter,
11    ublox_device::{Device, UbloxDriver},
12    utils::WheelBuffer,
13};
14use embedded_hal::blocking::delay::DelayMs;
15use fugit::{ExtU64, MillisDuration, MillisDurationU32};
16
17const BUFFER_LENGTH: usize = 5;
18/// Barometric data picked up at a specific timestamp
19// TODO make BaroData non-Copy
20#[derive(Debug, Clone, Copy)]
21pub struct BaroData {
22    /// Altitude above ground in meters
23    pub agl: f32,
24    /// Pressure in Pa
25    pub pressure: f32,
26    /// Timestamp in ms
27    pub timestamp: MillisDuration<u64>,
28}
29
30impl core::default::Default for BaroData {
31    fn default() -> Self {
32        Self {
33            timestamp: 0.millis(),
34            ..BaroData::default()
35        }
36    }
37}
38
39impl core::ops::Sub<BaroData> for BaroData {
40    type Output = Self;
41
42    #[inline]
43    fn sub(self, rhs: Self) -> Self::Output {
44        BaroData {
45            agl: self.agl - rhs.agl,
46            pressure: self.pressure - rhs.pressure,
47            timestamp: self.timestamp - rhs.timestamp,
48        }
49    }
50}
51
52/// Used to detect the current flight phase
53pub struct BaroComputer<A: Altimeter> {
54    /// Altitude measuring device
55    altimeter: A,
56    /// Buffer holding the last `BUFFER_LENGTH` baro samples
57    buf: WheelBuffer<BaroData, BUFFER_LENGTH>,
58    /// The current flight phase
59    current_phase: FlightPhase,
60    /// Number of pre-trigger checks that have been passed
61    num_pre_checks: usize,
62    /// Internal ground reference. Most importantly, holds the ground pressure
63    ground_ref: BaroData,
64    /// Absolute latest measurement. May be more recent than the most recent
65    /// measurement in `buf`.
66    latest_measurement: BaroData,
67}
68
69impl<A: Altimeter> BaroComputer<A> {
70    /// Create a new `BaroComputer`
71    #[inline]
72    pub fn new(altimeter: A, current_phase: FlightPhase, fill_buf: BaroData) -> Self {
73        Self {
74            altimeter,
75            current_phase,
76            buf: WheelBuffer::new(fill_buf),
77            num_pre_checks: 0,
78            ground_ref: fill_buf,
79            latest_measurement: fill_buf,
80        }
81    }
82
83    /// Return the current flight phase
84    #[inline]
85    pub fn current_phase(&self) -> FlightPhase {
86        self.current_phase
87    }
88
89    /// Return the computer's ground reference.
90    #[inline]
91    pub fn ground_ref(&self) -> BaroData {
92        self.ground_ref
93    }
94
95    /// Execute a barometric measurement. Will store in
96    /// `self.latest_measurement`
97    #[inline]
98    pub fn measure<D: DelayMs<u8>>(&mut self, delay: &mut D, timestamp: MillisDuration<u64>) {
99        self.latest_measurement = self.altimeter.measure(delay, self.ground_ref(), timestamp);
100    }
101
102    /// Return a shared reference to the computer's internal [`WheelBuffer`]
103    #[inline]
104    pub fn buf(&self) -> &WheelBuffer<BaroData, BUFFER_LENGTH> {
105        &self.buf
106    }
107
108    /// Return the latest barometric measurement
109    #[inline]
110    pub fn last_measurement(&self) -> BaroData {
111        self.latest_measurement
112    }
113
114    /// Notify that the GPS signal has been acquired, and that we can switch
115    /// from `FlightPhase::Startup` to `FlightPhase::Ground`
116    #[inline]
117    pub fn acquire_signal<D: UbloxDriver, const L: usize>(&mut self, gps: &mut Device<D, L>) {
118        gps.acquire_signal();
119
120        if self.current_phase == FlightPhase::Startup {
121            self.current_phase = FlightPhase::Ground;
122            gps.gnss_power(false);
123        }
124    }
125
126    /// Switch flight phases
127    ///
128    /// Compute whether the computer should switch to a new flight phase. Run a
129    /// closure __only__ if the phase has switched.
130    #[inline]
131    pub fn check_phase_switch<F: FnMut(FlightPhase, BaroData, &mut A)>(
132        &mut self,
133        mut on_switch_phase: F,
134    ) {
135        self.push_latest();
136
137        let old_phase = self.current_phase();
138
139        match old_phase {
140            FlightPhase::Startup | FlightPhase::Ground => {
141                if self.detect_climb() {
142                    self.current_phase = FlightPhase::Climb;
143                }
144            }
145
146            FlightPhase::Climb => {
147                if self.detect_freefall() {
148                    self.current_phase = FlightPhase::FlightLogging;
149                }
150            }
151
152            FlightPhase::FlightLogging => {
153                if self.detect_landing() {
154                    self.current_phase = FlightPhase::Ground;
155                }
156            }
157        }
158
159        let new_phase = self.current_phase();
160
161        if old_phase != new_phase {
162            on_switch_phase(new_phase, self.buf.latest(), &mut self.altimeter);
163        }
164    }
165
166    /// Add a new measurement to the computer's internal [`WheelBuffer`]
167    #[inline]
168    fn push_latest(&mut self) {
169        self.buf.push(self.last_measurement());
170    }
171
172    /// Compute the difference between the last two measurements in buffer
173    #[inline]
174    fn diff(&self) -> BaroData {
175        self.buf.peek(0).unwrap() - self.buf.peek(1).unwrap()
176    }
177
178    /// Detect if we should switch to climb mode from ground mode
179    #[inline]
180    fn detect_climb(&mut self) -> bool {
181        const REQD_PRE_CHECKS: usize = 2;
182        // Rates are divided by 1000 (meters per millisecond) because we store the
183        // timestamp as milliseconds
184        const TRIGGER_CLIMB_RATES: [f32; REQD_PRE_CHECKS + 1] =
185            [0.25 / 1000.0, 0.75 / 1000.0, 1.0 / 1000.0];
186        static_assertions::const_assert!(REQD_PRE_CHECKS < BUFFER_LENGTH);
187
188        let mut pre_trigger = false;
189
190        let climb_detected = self.detect_phase(REQD_PRE_CHECKS, |diff, num_pre_checks| {
191            pre_trigger =
192                diff.agl >= TRIGGER_CLIMB_RATES[num_pre_checks] * diff.timestamp.ticks() as f32;
193
194            pre_trigger
195        });
196
197        // Reset ground pressure only if there was no pre-trigger (no climb is
198        // suspected)
199        //
200        // Also, we are a taking a ground pressure further back in time (8 * 4 = 32
201        // seconds) to try to pick up a pressure which was valid *before* the aircraft
202        // started rolling, which could mess with pressure measurements.
203        if !pre_trigger {
204            self.set_ground_ref(self.buf().peek(4).unwrap());
205        }
206
207        climb_detected
208    }
209
210    /// Detect if we should switch to ground mode from freefall mode
211    #[inline]
212    fn detect_landing(&mut self) -> bool {
213        // Rates are divided by 1000 (meters per millisecond) because we store the
214        // timestamp as milliseconds
215        const LANDING_RATE_MAX: f32 = 3.0 / 1000.0;
216        const REQD_PRE_CHECKS: usize = 19;
217
218        self.detect_phase(REQD_PRE_CHECKS, |diff, _| {
219            diff.agl.abs() <= LANDING_RATE_MAX * diff.timestamp.ticks() as f32
220        })
221    }
222
223    /// Detect if we should switch to freefall mode from climb mod
224    #[inline]
225    fn detect_freefall(&mut self) -> bool {
226        const REQD_PRE_CHECKS: usize = 3;
227        // Rates are divided by 1000 (meters per millisecond) because we store the
228        // timestamp as milliseconds
229        const TRIGGER_FREEFALL_RATES: [f32; REQD_PRE_CHECKS + 1] =
230            [-3.0 / 1000.0, -5.0 / 1000.0, -8.0 / 1000.0, -10.0 / 1000.0];
231
232        self.detect_phase(REQD_PRE_CHECKS, |diff, num_pre_checks| {
233            diff.agl <= TRIGGER_FREEFALL_RATES[num_pre_checks] * diff.timestamp.ticks() as f32
234        })
235    }
236
237    /// # Check if we should switch flight phases.
238    ///
239    /// This function takes a closure that accepts a [`BaroData`] which
240    /// represents the difference between the last and next last baro
241    /// measurements.
242    #[inline]
243    fn detect_phase<F>(&mut self, reqd_pre_checks: usize, mut func: F) -> bool
244    where
245        F: FnMut(BaroData, usize) -> bool,
246    {
247        // TODO check that the timestamp difference is large enough (eg 8 seconds)
248        // (unneeded if the time diff comes from the FlightPhase impl?)
249        let diff = self.diff();
250
251        // n-stage check: check was passed
252        if func(diff, self.num_pre_checks) {
253            // Final check: switch mode
254            if self.num_pre_checks >= reqd_pre_checks {
255                self.num_pre_checks = 0;
256                return true;
257                // This wasn't the final check; increment pre-trigger counter
258            }
259
260            self.num_pre_checks += 1;
261
262        // Check wasn't passed; reset pre-trigger counter
263        } else {
264            self.num_pre_checks = 0;
265        }
266
267        false
268    }
269
270    /// Set the computer's internal ground reference
271    #[inline]
272    fn set_ground_ref(&mut self, ground_ref: BaroData) {
273        self.ground_ref = ground_ref;
274    }
275}
276
277/// The flight phase describes the current state of the device
278#[derive(Clone, Copy, Debug, PartialEq)]
279pub enum FlightPhase {
280    /// Device startup
281    Startup,
282    /// Active data logging in flight
283    FlightLogging,
284    /// On the ground; measure pressure to detect takeoff
285    Ground,
286    /// Climbing in aircraft; wait for exit
287    Climb,
288}
289
290impl FlightPhase {
291    /// Time after which a new phase check should be done (in ms)
292    #[inline]
293    pub const fn phase_check_wait_time(&self) -> MillisDuration<u32> {
294        match self {
295            FlightPhase::Startup | FlightPhase::Ground => MillisDurationU32::from_ticks(10_000),
296            FlightPhase::FlightLogging | FlightPhase::Climb => MillisDurationU32::from_ticks(1000),
297        }
298    }
299
300    pub const FLIGHT_LOGGING_MEASUREMENT_INTERVAL: MillisDuration<u32> =
301        MillisDurationU32::from_ticks(50);
302}