1use crate::frames::Ned;
10use crate::state::{ErrorState, Offset, State};
11use crate::units::{
12 Altitude, AltitudeNoise, Position, PositionNoise, Timestamp, Velocity, VelocityNoise,
13};
14
15use super::Eskf;
16
17impl Eskf {
18 #[must_use = "a refused reset leaves the estimate where it was, still dead-reckoning"]
39 pub fn reset_position_to(
40 &mut self,
41 position: Position<Ned>,
42 noise: PositionNoise<Ned>,
43 ) -> bool {
44 if !position.is_finite() || !noise.is_finite() || !noise.is_positive() {
45 return false;
46 }
47 self.adopt_position(position, noise, POSITION);
48 self.unestablished.position = false;
49 true
50 }
51
52 #[must_use = "a refused reset leaves the estimate where it was, still dead-reckoning"]
56 pub fn reset_velocity_to(
57 &mut self,
58 velocity: Velocity<Ned>,
59 noise: VelocityNoise<Ned>,
60 ) -> bool {
61 if !velocity.is_finite() || !noise.is_finite() || !noise.is_positive() {
62 return false;
63 }
64 self.adopt_velocity(velocity, noise);
65 self.unestablished.velocity = false;
66 true
67 }
68
69 #[inline(never)]
75 pub(super) fn adopt_position<const N: usize>(
76 &mut self,
77 position: Position<Ned>,
78 noise: PositionNoise<Ned>,
79 axes: [ErrorState; N],
80 ) {
81 let (z, r) = (position.vector(), noise.variance());
82 let mut adopted = self.estimate.state().position.vector();
83 let mut variances = [0.0; N];
84 for (axis, variance) in axes.iter().zip(&mut variances) {
85 let i = axis
88 .index()
89 .saturating_sub(ErrorState::PositionNorth.index());
90 if let (Some(adopted), Some(z), Some(r)) = (adopted.get_mut(i), z.get(i), r.get(i)) {
91 *adopted = *z;
92 *variance = *r;
93 }
94 }
95 self.estimate.commit(State {
96 position: Position::ned(adopted[0], adopted[1], adopted[2]),
97 ..*self.estimate.state()
98 });
99 self.reset_block(axes, variances);
100 if axes.contains(&ErrorState::PositionNorth) {
101 self.hold.end_position();
102 self.yaw_replaced.settle_position();
103 }
104 }
105
106 pub(super) fn adopt_height(&mut self, position: Position<Ned>, noise: PositionNoise<Ned>) {
117 self.adopt_position(position, noise, [ErrorState::PositionDown]);
118 if self.config.baro_reference_from_estimate {
119 self.establish_reference(None);
120 }
121 }
122
123 #[inline(never)]
126 pub(super) fn adopt_velocity(&mut self, velocity: Velocity<Ned>, noise: VelocityNoise<Ned>) {
127 self.estimate.commit(State {
128 velocity,
129 ..*self.estimate.state()
130 });
131 self.reset_block(
132 [
133 ErrorState::VelocityNorth,
134 ErrorState::VelocityEast,
135 ErrorState::VelocityDown,
136 ],
137 noise.variance(),
138 );
139 self.hold.end_velocity();
140 self.yaw_replaced.settle_velocity();
141 }
142
143 pub(super) fn reference_from_estimate(
148 &mut self,
149 altitude: Altitude,
150 noise: AltitudeNoise,
151 time: Timestamp,
152 ) {
153 let (past, _) = self.past(time);
155 let reference = Altitude::from_meters(altitude.as_meters() + past.position.vector()[2]);
156 let offset = Offset::from_estimate(&self.covariance, noise.variance());
157 self.establish_reference(Some((reference, offset)));
158 }
159
160 fn reset_block<const N: usize>(&mut self, states: [ErrorState; N], variances: [f32; N]) {
164 let mut covariance = self.covariance;
165 covariance.reset_block(states, variances);
166 let mut offset = self.offset;
167 for state in states {
168 offset.decorrelate(state);
169 }
170 self.commit_covariance(covariance, offset);
171 }
172}
173
174pub(super) const POSITION: [ErrorState; 3] = [
176 ErrorState::PositionNorth,
177 ErrorState::PositionEast,
178 ErrorState::PositionDown,
179];
180
181pub(super) const HORIZONTAL: [ErrorState; 2] =
183 [ErrorState::PositionNorth, ErrorState::PositionEast];
184
185#[cfg(test)]
186mod tests {
187
188 use crate::config::{Config, GRAVITY, Recovery};
189 use crate::eskf::Eskf;
190 use crate::eskf::fixtures::*;
191 use crate::frames::Ned;
192
193 use crate::health::{Fusion, GnssFusion, Propagation};
194 use crate::init::tests::still;
195
196 use crate::propagate::ImuSample;
197 use crate::state::{Covariance, ErrorState};
198 use crate::units::{
199 Acceleration, Altitude, AltitudeNoise, AngularRate, Position, PositionNoise, Seconds,
200 Velocity, VelocityNoise,
201 };
202
203 #[test]
204 fn an_adopted_measurement_is_counted_apart_from_an_ordinary_acceptance() {
205 let mut filter = coarse();
206 let noise = PositionNoise::horizontal_vertical(1.5, 1.5);
207 assert_eq!(
208 filter.fuse_gnss_position(
209 filter.now(),
210 Position::ned(120.0, -40.0, -75.0),
211 noise,
212 Position::zero()
213 ),
214 GnssFusion::both(Fusion::Reset)
215 );
216 assert!(
218 filter
219 .fuse_gnss_position(
220 filter.now(),
221 Position::ned(121.0, -40.0, -75.0),
222 noise,
223 Position::zero()
224 )
225 .is_accepted()
226 );
227
228 let health = filter.diagnostics().gnss_position;
229 assert_eq!(health.adopted, 1, "adoption happens once per quantity");
230 assert_eq!(health.accepted, 2, "and counts as an acceptance besides");
231 }
232
233 #[test]
234 fn an_adopted_position_carries_no_correlation_with_the_reference() {
235 let mut filter = aided();
236 for _ in 0..100 {
237 assert_eq!(filter.step(still().imu, DT), Propagation::Propagated);
238 }
239 let _ = filter.fuse_baro_altitude(
240 filter.now(),
241 Altitude::from_meters(100.0),
242 AltitudeNoise::from_sigma(0.5),
243 );
244 let down = ErrorState::PositionDown.index();
245 assert!(
246 filter.offset.cross[down] != 0.0,
247 "the altitude correlated the two"
248 );
249
250 assert!(filter.reset_position_to(
251 Position::ned(1.0, 2.0, -3.0),
252 PositionNoise::from_sigma(1.0, 1.0, 1.0)
253 ));
254 assert_eq!(filter.offset.cross.fixed_rows::<3>(0).norm(), 0.0);
255 }
256
257 #[test]
258 fn a_reset_below_the_floor_is_floored_and_counted_rather_than_refused() {
259 let mut filter = aided();
264 assert_eq!(filter.diagnostics().floored, 0);
265
266 assert!(filter.reset_position_to(
267 Position::ned(10.0, 20.0, -5.0),
268 PositionNoise::from_sigma(1e-15, 1e-15, 1e-15),
269 ));
270
271 assert_eq!(filter.diagnostics().floored, 3);
272 assert!(filter.covariance().variance(ErrorState::PositionNorth) >= 1e-6);
273 }
274
275 #[test]
276 fn after_a_coarse_start_the_first_fix_is_adopted_not_fused() {
277 let mut filter = coarse();
278 let fix = Position::ned(120.0, -40.0, -75.0);
279 let outcome = filter.fuse_gnss_position(
280 filter.now(),
281 fix,
282 PositionNoise::horizontal_vertical(1.5, 1.5),
283 Position::zero(),
284 );
285
286 assert_eq!(outcome, GnssFusion::both(Fusion::Reset));
287 assert!(outcome.is_accepted(), "the measurement was used");
288 assert!(outcome.is_reset(), "and it stepped the state");
289 assert_eq!(filter.state().position, fix);
290 assert!(
291 (filter.covariance().variance(ErrorState::PositionNorth) - 2.25).abs() < 1e-6,
292 "the fix's own variance, not the configured prior"
293 );
294
295 let outcome = filter.fuse_gnss_position(
298 filter.now(),
299 fix,
300 PositionNoise::horizontal_vertical(1.5, 1.5),
301 Position::zero(),
302 );
303 assert!(
304 matches!(outcome.horizontal, Fusion::Accepted { .. })
305 && matches!(outcome.height, Fusion::Accepted { .. }),
306 "{outcome:?}"
307 );
308 assert_eq!(filter.diagnostics().gnss_position.adopted, 1);
309 }
310
311 #[test]
312 fn velocity_is_adopted_too_and_the_two_are_independent() {
313 let mut filter = coarse();
314 let velocity = Velocity::ned(18.0, 1.0, -0.5);
315 assert!(
316 filter
317 .fuse_gnss_velocity(
318 filter.now(),
319 velocity,
320 VelocityNoise::from_speed_accuracy(0.3),
321 Position::zero()
322 )
323 .is_reset()
324 );
325 assert_eq!(filter.state().velocity, velocity);
326
327 assert!(
329 filter
330 .fuse_gnss_position(
331 filter.now(),
332 Position::ned(1.0, 2.0, 3.0),
333 PositionNoise::horizontal_vertical(1.5, 1.5),
334 Position::zero()
335 )
336 .is_reset()
337 );
338 }
339
340 #[test]
341 fn a_static_start_knows_where_it_is_so_its_first_fix_is_fused() {
342 let mut filter = initialized();
343 let outcome = filter.fuse_gnss_position(
344 filter.now(),
345 Position::ned(0.2, -0.1, 0.0),
346 PositionNoise::horizontal_vertical(1.5, 1.5),
347 Position::zero(),
348 );
349 assert!(
350 !outcome.is_reset(),
351 "the origin is where it was, by definition"
352 );
353 assert!(outcome.is_accepted());
354 }
355
356 #[test]
357 fn a_reset_drops_the_correlations_the_old_estimate_had() {
358 let mut filter = coarse();
359 let mut matrix = *filter.covariance().as_matrix();
361 let (p_n, v_n) = (
362 ErrorState::PositionNorth.index(),
363 ErrorState::VelocityNorth.index(),
364 );
365 matrix[(p_n, v_n)] = 0.5;
366 matrix[(v_n, p_n)] = 0.5;
367 filter.covariance = Covariance::from_matrix(matrix);
368
369 assert!(filter.reset_position_to(
370 Position::ned(10.0, 0.0, 0.0),
371 PositionNoise::horizontal_vertical(1.0, 1.0),
372 ));
373
374 let after = filter.covariance().as_matrix();
375 assert_eq!(after[(p_n, v_n)], 0.0, "the new error came from the fix");
376 assert_eq!(after[(v_n, p_n)], 0.0);
377 assert_eq!(after[(p_n, p_n)], 1.0);
378 assert!(
379 after[(v_n, v_n)] > 0.0,
380 "resetting position must not disturb velocity"
381 );
382 }
383
384 #[test]
385 fn a_seed_is_trusted_and_is_never_overwritten_by_a_fix() {
386 let (state, covariance) = seed();
387 let mut filter = Eskf::default();
388 let _ = filter.seed(state, covariance).expect("a sane seed");
389 let outcome = filter.fuse_gnss_velocity(
390 filter.now(),
391 Velocity::ned(0.0, 0.0, 0.0),
392 VelocityNoise::from_speed_accuracy(0.3),
393 Position::zero(),
394 );
395 assert!(
396 !outcome.is_reset(),
397 "the caller vouched for this velocity; its covariance says how far"
398 );
399 assert_eq!(filter.state().velocity, state.velocity);
400 }
401
402 #[test]
403 fn an_adopted_fix_is_referred_to_the_imu() {
404 let mut filter = coarse();
405 let yaw = filter.state().attitude.quaternion();
406 let at_antenna = Position::ned(10.0, 20.0, -5.0);
407 let noise = PositionNoise::horizontal_vertical(0.5, 0.5);
408 let adopted = filter.fuse_gnss_position(filter.now(), at_antenna, noise, mast());
409 assert!(adopted.is_reset(), "{adopted:?}");
410 let imu = at_antenna.vector() - yaw * mast().vector();
411 assert!(near(filter.state().position, Position::from_vector(imu)));
412 }
413
414 #[test]
415 fn an_adopted_velocity_is_the_imus_not_the_antennas_swing() {
416 let mut filter = coarse();
417 let imu = ImuSample::reading(
418 AngularRate::body(0.0, 0.0, 0.5),
419 Acceleration::body(0.0, 0.0, -GRAVITY),
420 );
421 assert!(filter.step(imu, DT).is_propagated());
422 let rotation = filter.state().attitude.quaternion();
423 let omega = filter
424 .angular_rate()
425 .expect("a step was integrated")
426 .vector();
427 let at_antenna = Velocity::ned(3.0, -2.0, 0.5);
428 let noise = VelocityNoise::from_speed_accuracy(0.3);
429 let adopted = filter.fuse_gnss_velocity(filter.now(), at_antenna, noise, mast());
430 assert!(adopted.is_reset(), "{adopted:?}");
431 let imu = at_antenna.vector() - rotation * omega.cross(&mast().vector());
432 let got = filter.state().velocity.vector();
433 assert!((got - imu).norm() < 1e-4, "{got} against {imu}");
434 }
435
436 #[test]
437 fn a_coarse_start_does_not_adopt_a_fix_whose_noise_is_impossible() {
438 let mut filter = coarse();
442 let fix = Position::ned(120.0, -40.0, -75.0);
443 assert_eq!(
444 filter.fuse_gnss_position(
445 filter.now(),
446 fix,
447 PositionNoise::<Ned>::from_variance(-1.0, -1.0, -1.0),
448 Position::zero()
449 ),
450 GnssFusion::both(Fusion::InvalidNoise)
451 );
452 assert_eq!(filter.state().position, Position::zero(), "nothing adopted");
453 assert!(!filter.validity().horizontal_position);
454 assert!(
455 filter
456 .fuse_gnss_position(
457 filter.now(),
458 fix,
459 PositionNoise::horizontal_vertical(1.5, 1.5),
460 Position::zero()
461 )
462 .is_reset(),
463 "the adoption is still owed to the first usable fix"
464 );
465 }
466
467 #[test]
468 fn an_external_reset_refuses_what_would_poison_the_state() {
469 let mut filter = initialized();
470 assert!(!filter.reset_position_to(
471 Position::ned(f32::NAN, 0.0, 0.0),
472 PositionNoise::horizontal_vertical(1.5, 1.5)
473 ));
474 assert!(!filter.reset_position_to(
475 Position::ned(10.0, 0.0, 0.0),
476 PositionNoise::<Ned>::from_variance(1.0, 0.0, 1.0)
477 ));
478 assert_eq!(
479 filter.state().position,
480 Position::zero(),
481 "a refused reset changes nothing"
482 );
483
484 assert!(!filter.reset_velocity_to(
485 Velocity::ned(1.0, 0.0, 0.0),
486 VelocityNoise::<Ned>::from_variance(1.0, 1.0, -0.25)
487 ));
488 assert_eq!(filter.state().velocity, Velocity::zero());
489
490 assert!(!filter.set_baro_reference(
491 Altitude::from_meters(f32::NAN),
492 AltitudeNoise::from_sigma(0.001)
493 ));
494 assert_eq!(
495 filter.baro_reference(),
496 None,
497 "a NaN alpha_0 would end barometric aiding for the flight, not one update"
498 );
499 assert!(filter.set_baro_reference(
500 Altitude::from_meters(52.0),
501 AltitudeNoise::from_sigma(0.001)
502 ));
503 }
504
505 fn far() -> Position<Ned> {
509 Position::ned(1000.0, 0.0, 0.0)
510 }
511
512 #[test]
513 fn a_fix_rejected_past_the_timeout_is_adopted_and_its_height_is_left_to_its_own_gate() {
514 let mut filter = unheld();
515 hold(&mut filter, 6.9, 100, |filter| {
518 let outcome =
519 filter.fuse_gnss_position(filter.now(), far(), one_metre(), Position::zero());
520 assert!(
521 matches!(outcome.horizontal, Fusion::Rejected { .. }),
522 "{outcome:?}"
523 );
524 });
525 hold(&mut filter, 0.1, 10, |_| {});
526
527 let fix = Position::ned(1000.0, 0.0, 0.5);
530 let outcome = filter.fuse_gnss_position(filter.now(), fix, one_metre(), Position::zero());
531 assert_eq!(outcome.horizontal, Fusion::Reset);
532 assert!(
533 matches!(outcome.height, Fusion::Accepted { .. }),
534 "the height agreed, so it is fused rather than adopted: {outcome:?}"
535 );
536 let position = filter.state().position.vector();
537 assert_eq!((position[0], position[1]), (1000.0, 0.0));
538 assert!(
539 position[2] < 0.4,
540 "fused part of the way, not adopted: {}",
541 position[2]
542 );
543 assert!(
544 (filter.covariance().variance(ErrorState::PositionNorth) - 1.0).abs() < 1e-6,
545 "the fix's variance, which is what undoes the lockout"
546 );
547 let d = filter.diagnostics();
548 assert_eq!((d.gnss_position.recovered, d.gnss_position.adopted), (1, 1));
549 assert_eq!(d.gnss_height.recovered, 0);
550
551 let outcome = filter.fuse_gnss_position(filter.now(), far(), one_metre(), Position::zero());
553 assert!(
554 matches!(outcome.horizontal, Fusion::Accepted { .. }),
555 "{outcome:?}"
556 );
557 }
558
559 #[test]
560 fn a_fix_that_agrees_after_a_long_silence_is_fused_not_adopted() {
561 let mut filter = initialized();
563 hold(&mut filter, 10.0, 1000, |_| {});
564 let outcome = filter.fuse_gnss_position(
565 filter.now(),
566 Position::zero(),
567 one_metre(),
568 Position::zero(),
569 );
570 assert!(
571 matches!(outcome.horizontal, Fusion::Accepted { .. })
572 && matches!(outcome.height, Fusion::Accepted { .. }),
573 "{outcome:?}"
574 );
575 assert_eq!(filter.diagnostics().gnss_position.adopted, 0);
576 }
577
578 #[test]
579 fn a_named_reference_is_kept_through_a_barometer_lockout() {
580 let mut filter = Eskf::new(Config {
583 baro_reference_from_estimate: false,
584 ..Config::default()
585 })
586 .unwrap();
587 let _ = filter
588 .initialize_over(&[still(); 8], Seconds::from_secs(0.25))
589 .expect("a 2 s window of stillness");
590 assert!(
591 filter.set_baro_reference(Altitude::from_meters(100.0), AltitudeNoise::from_sigma(0.1))
592 );
593 let mut step = 0;
594 hold(&mut filter, 8.0, 10, |filter| {
595 step += 1;
596 if step % 10 == 0 {
597 let _ = filter.fuse_gnss_position(
598 filter.now(),
599 Position::zero(),
600 one_metre(),
601 Position::zero(),
602 );
603 }
604 let outcome = filter.fuse_baro_altitude(
605 filter.now(),
606 Altitude::from_meters(150.0),
607 AltitudeNoise::from_sigma(0.5),
608 );
609 assert!(matches!(outcome, Fusion::Rejected { .. }), "{outcome:?}");
610 });
611 let reference = filter.baro_reference().expect("named").as_meters();
612 assert!((reference - 100.0).abs() < 0.5, "α₀ = {reference}");
613 assert_eq!(filter.diagnostics().baro_altitude.recovered, 0);
614 }
615
616 #[test]
617 fn a_barometer_is_not_re_referenced_before_position_is_established() {
618 let mut filter = aided();
621 let mut window = [still(); 8];
622 window[3].imu = window[3].imu.with_gyro(AngularRate::body(0.0, 0.4, 0.0));
623 let _ = filter
624 .initialize_over(&window, Seconds::from_secs(0.25))
625 .expect("moving, not unusable");
626 hold(&mut filter, 8.0, 10, |filter| {
627 let outcome = filter.fuse_baro_altitude(
628 filter.now(),
629 Altitude::from_meters(5000.0),
630 AltitudeNoise::from_sigma(0.5),
631 );
632 assert!(matches!(outcome, Fusion::Rejected { .. }), "{outcome:?}");
633 });
634 assert_eq!(filter.baro_reference(), Some(Altitude::from_meters(100.0)));
635 }
636
637 #[test]
638 fn with_recovery_off_a_locked_out_source_is_rejected_for_good() {
639 let mut filter = Eskf::new(Config {
640 recovery: Recovery::OFF,
641 ..Config::default()
642 })
643 .unwrap();
644 let _ = filter
645 .initialize_over(&[still(); 8], Seconds::from_secs(0.25))
646 .expect("a 2 s window of stillness");
647 hold(&mut filter, 15.0, 100, |filter| {
650 let outcome =
651 filter.fuse_gnss_position(filter.now(), far(), one_metre(), Position::zero());
652 assert!(
653 matches!(outcome.horizontal, Fusion::Rejected { .. }),
654 "{outcome:?}"
655 );
656 });
657 assert_eq!(filter.diagnostics().gnss_position.adopted, 0);
658 assert!(filter.state().position.vector()[0].abs() < 1.0);
659 }
660
661 #[test]
662 fn only_a_rejection_recovers_and_a_refusal_never_does() {
663 let mut filter = initialized();
664 let nan = PositionNoise::from_sigma(f32::NAN, f32::NAN, f32::NAN);
665 hold(&mut filter, 10.0, 100, |filter| {
666 assert_eq!(
667 filter.fuse_gnss_position(filter.now(), far(), nan, Position::zero()),
668 GnssFusion::both(Fusion::NotFinite)
669 );
670 });
671 assert!(filter.state().position.vector()[0].abs() < 1.0);
672
673 let outcome = filter.fuse_gnss_position(filter.now(), far(), one_metre(), Position::zero());
676 assert_eq!(outcome.horizontal, Fusion::Reset);
677 }
678
679 #[test]
680 fn a_locked_out_velocity_is_adopted() {
681 let mut filter = unheld();
682 let velocity = Velocity::ned(20.0, 0.0, 0.0);
683 let noise = VelocityNoise::from_speed_accuracy(0.3);
684 hold(&mut filter, 6.9, 100, |filter| {
685 let outcome =
686 filter.fuse_gnss_velocity(filter.now(), velocity, noise, Position::zero());
687 assert!(matches!(outcome, Fusion::Rejected { .. }), "{outcome:?}");
688 });
689 hold(&mut filter, 0.1, 10, |_| {});
690 assert_eq!(
691 filter.fuse_gnss_velocity(filter.now(), velocity, noise, Position::zero()),
692 Fusion::Reset
693 );
694 assert_eq!(filter.state().velocity, velocity);
695 assert_eq!(filter.diagnostics().gnss_velocity.recovered, 1);
696 }
697
698 #[test]
699 fn an_adopted_velocity_is_established_and_the_next_solution_is_fused() {
700 let mut filter = coarse();
701 let velocity = Velocity::ned(18.0, 1.0, -0.5);
702 let noise = VelocityNoise::from_speed_accuracy(0.3);
703 assert!(
704 filter
705 .fuse_gnss_velocity(filter.now(), velocity, noise, Position::zero())
706 .is_reset()
707 );
708 assert!(
709 matches!(
710 filter.fuse_gnss_velocity(filter.now(), velocity, noise, Position::zero()),
711 Fusion::Accepted { .. }
712 ),
713 "once is once"
714 );
715 assert_eq!(filter.diagnostics().gnss_velocity.adopted, 1);
716 }
717
718 #[test]
719 fn a_locked_out_height_is_adopted_and_the_barometer_reads_its_reference_again() {
720 let mut filter = aided();
723 let fix = Position::ned(0.0, 0.0, -20.0);
724 let baro = AltitudeNoise::from_sigma(0.5);
725 let mut adopted_at = None;
726 let mut step = 0;
727 hold(&mut filter, 6.0, 10, |filter| {
728 step += 1;
729 if adopted_at.is_some() {
730 return;
731 }
732 if step % 10 == 0 {
733 let outcome =
734 filter.fuse_gnss_position(filter.now(), fix, one_metre(), Position::zero());
735 if outcome.height == Fusion::Reset {
736 adopted_at.get_or_insert(step);
737 assert_eq!(filter.state().position.vector()[2], -20.0);
738 assert_eq!(filter.baro_reference(), None, "dropped with the height");
739 assert_eq!(
741 filter.fuse_baro_altitude(filter.now(), Altitude::from_meters(100.0), baro),
742 Fusion::Accepted { test_ratio: 0.0 }
743 );
744 let reference = filter.baro_reference().expect("read again").as_meters();
745 assert!((reference - 80.0).abs() < 1e-3, "α₀ = {reference}");
746 return;
747 }
748 assert!(
749 matches!(outcome.height, Fusion::Rejected { .. }),
750 "{outcome:?}"
751 );
752 }
753 let _ = filter.fuse_baro_altitude(filter.now(), Altitude::from_meters(100.0), baro);
754 });
755 let at = adopted_at.expect("recovered within 6 s") as f32 * 0.1;
756 assert!((5.0..=5.1).contains(&at), "at {at} s");
757 assert_eq!(filter.diagnostics().gnss_height.recovered, 1);
758 assert_eq!(filter.diagnostics().gnss_position.recovered, 0);
759 }
760
761 #[test]
762 fn a_locked_out_barometer_reads_its_reference_again_and_moves_nothing() {
763 let mut filter = aided();
764 let baro = AltitudeNoise::from_sigma(0.5);
765 let mut step = 0;
766 let mut recovered = false;
767 hold(&mut filter, 6.0, 10, |filter| {
768 step += 1;
769 if step % 10 == 0 {
770 let _ = filter.fuse_gnss_position(
771 filter.now(),
772 Position::zero(),
773 one_metre(),
774 Position::zero(),
775 );
776 }
777 if recovered {
778 return;
779 }
780 let before = filter.state();
781 match filter.fuse_baro_altitude(filter.now(), Altitude::from_meters(150.0), baro) {
782 Fusion::Rejected { .. } => {}
783 Fusion::Reset => {
784 recovered = true;
785 assert_eq!(filter.state(), before, "a reference, not a state");
786 let reference = filter.baro_reference().expect("read again").as_meters();
787 let down = before.position.vector()[2];
788 assert!(
789 (reference - (150.0 + down)).abs() < 1e-3,
790 "α₀ = {reference}"
791 );
792 }
793 outcome => panic!("{outcome:?}"),
794 }
795 });
796 assert!(recovered);
797 assert_eq!(filter.diagnostics().baro_altitude.recovered, 1);
798 }
799}