1use super::loose::{GnssFixMeasurement, InertialFilter};
4use super::state::{
5 validate_covariance_matrix, validate_finite_slice, ErrorStateLayout, ErrorStateVector,
6 InsFilterState,
7};
8use super::tight::{
9 augmented_dimension, TightCarrierPhaseObservation, TightFilterSnapshot, TightGnssEpoch,
10 TightGnssObservation, TightRangeRateObservation,
11};
12use super::timesync::{
13 InertialFilterSnapshot, RateEndpoint, StoredCheckpoint, StoredGnssMeasurement, StoredImuSample,
14 TimeSyncHistoryConfig, TimeSyncHistorySnapshot,
15};
16use crate::inertial::{ImuSample, ImuSampleKind, NavState};
17use crate::{GnssSatelliteId, GnssSystem};
18
19const FUSION_STATE_MAGIC: [u8; 8] = *b"FUSSTAT\0";
20const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
21const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
22
23pub const FUSION_STATE_CODEC_VERSION: u16 = 1;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
28#[serde(transparent)]
29pub struct F64Bits {
30 pub bits: u64,
32}
33
34impl F64Bits {
35 pub const fn from_f64(value: f64) -> Self {
37 Self {
38 bits: value.to_bits(),
39 }
40 }
41
42 pub const fn to_f64(self) -> f64 {
44 f64::from_bits(self.bits)
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50pub enum SerializableErrorStateLayout {
51 Fifteen,
53 TwentyOne,
55}
56
57impl SerializableErrorStateLayout {
58 pub const fn from_native(layout: ErrorStateLayout) -> Self {
60 match layout {
61 ErrorStateLayout::Fifteen => Self::Fifteen,
62 ErrorStateLayout::TwentyOne => Self::TwentyOne,
63 }
64 }
65
66 pub const fn to_native(self) -> ErrorStateLayout {
68 match self {
69 Self::Fifteen => ErrorStateLayout::Fifteen,
70 Self::TwentyOne => ErrorStateLayout::TwentyOne,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
77pub struct SerializableNavState {
78 pub t_j2000_s: F64Bits,
80 pub position_ecef_m: [F64Bits; 3],
82 pub velocity_ecef_mps: [F64Bits; 3],
84 pub attitude_body_to_ecef: [[F64Bits; 3]; 3],
86 pub accel_bias_mps2: [F64Bits; 3],
88 pub gyro_bias_rps: [F64Bits; 3],
90}
91
92impl SerializableNavState {
93 pub fn from_native(state: &NavState) -> Self {
95 Self {
96 t_j2000_s: F64Bits::from_f64(state.t_j2000_s),
97 position_ecef_m: bits3(state.position_ecef_m),
98 velocity_ecef_mps: bits3(state.velocity_ecef_mps),
99 attitude_body_to_ecef: bits3x3(state.attitude_body_to_ecef),
100 accel_bias_mps2: bits3(state.accel_bias_mps2),
101 gyro_bias_rps: bits3(state.gyro_bias_rps),
102 }
103 }
104
105 pub fn to_native(&self) -> Result<NavState, FusionStateCodecError> {
107 let state = NavState {
108 t_j2000_s: self.t_j2000_s.to_f64(),
109 position_ecef_m: f643(self.position_ecef_m),
110 velocity_ecef_mps: f643(self.velocity_ecef_mps),
111 attitude_body_to_ecef: f643x3(self.attitude_body_to_ecef),
112 accel_bias_mps2: f643(self.accel_bias_mps2),
113 gyro_bias_rps: f643(self.gyro_bias_rps),
114 };
115 state.validate().map_err(invalid_state)?;
116 Ok(state)
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122pub struct SerializableInsFilterState {
123 pub layout: SerializableErrorStateLayout,
125 pub nominal: SerializableNavState,
127 pub error_state: Vec<F64Bits>,
129 pub covariance: Vec<Vec<F64Bits>>,
131 pub accel_scale_factor: [F64Bits; 3],
133 pub gyro_scale_factor: [F64Bits; 3],
135}
136
137impl SerializableInsFilterState {
138 pub fn from_native(state: &InsFilterState) -> Self {
140 Self {
141 layout: SerializableErrorStateLayout::from_native(state.layout()),
142 nominal: SerializableNavState::from_native(&state.nominal),
143 error_state: bits_slice(state.error_state.as_slice()),
144 covariance: bits_matrix(&state.covariance),
145 accel_scale_factor: bits3(state.accel_scale_factor),
146 gyro_scale_factor: bits3(state.gyro_scale_factor),
147 }
148 }
149
150 pub fn to_native(&self) -> Result<InsFilterState, FusionStateCodecError> {
152 let layout = self.layout.to_native();
153 let nominal = self.nominal.to_native()?;
154 let error_state = ErrorStateVector::from_vec(layout, f64_vec(&self.error_state))
155 .map_err(invalid_state)?;
156 let state = InsFilterState {
157 nominal,
158 error_state,
159 covariance: f64_matrix(&self.covariance),
160 accel_scale_factor: f643(self.accel_scale_factor),
161 gyro_scale_factor: f643(self.gyro_scale_factor),
162 };
163 state.validate().map_err(invalid_state)?;
164 Ok(state)
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
170pub struct SerializableTightFilterState {
171 pub clock_bias_m: F64Bits,
173 pub clock_drift_m_s: F64Bits,
175 pub augmented_covariance: Vec<Vec<F64Bits>>,
177}
178
179impl SerializableTightFilterState {
180 pub fn from_native(snapshot: &TightFilterSnapshot) -> Self {
182 Self {
183 clock_bias_m: F64Bits::from_f64(snapshot.clock_bias_m),
184 clock_drift_m_s: F64Bits::from_f64(snapshot.clock_drift_m_s),
185 augmented_covariance: bits_matrix(&snapshot.augmented_covariance),
186 }
187 }
188
189 pub fn to_native(
191 &self,
192 base_dimension: usize,
193 ) -> Result<TightFilterSnapshot, FusionStateCodecError> {
194 let snapshot = TightFilterSnapshot {
195 clock_bias_m: self.clock_bias_m.to_f64(),
196 clock_drift_m_s: self.clock_drift_m_s.to_f64(),
197 augmented_covariance: f64_matrix(&self.augmented_covariance),
198 };
199 validate_finite_slice(
200 &[snapshot.clock_bias_m, snapshot.clock_drift_m_s],
201 "tight_clock",
202 )
203 .map_err(invalid_state)?;
204 validate_covariance_matrix(
205 &snapshot.augmented_covariance,
206 augmented_dimension(base_dimension),
207 "tight_augmented_covariance",
208 )
209 .map_err(invalid_state)?;
210 Ok(snapshot)
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
216pub struct SerializableTimeSyncHistoryConfig {
217 pub imu_capacity: u32,
219 pub checkpoint_capacity: u32,
221}
222
223impl SerializableTimeSyncHistoryConfig {
224 pub fn from_native(config: TimeSyncHistoryConfig) -> Result<Self, FusionStateCodecError> {
226 Ok(Self {
227 imu_capacity: checked_u32(config.imu_capacity)?,
228 checkpoint_capacity: checked_u32(config.checkpoint_capacity)?,
229 })
230 }
231
232 pub fn to_native(self) -> Result<TimeSyncHistoryConfig, FusionStateCodecError> {
234 let config = TimeSyncHistoryConfig::new(
235 self.imu_capacity as usize,
236 self.checkpoint_capacity as usize,
237 );
238 config.validate().map_err(invalid_state)?;
239 Ok(config)
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
245pub struct SerializableSatelliteId {
246 pub system: GnssSystem,
248 pub prn: u8,
250}
251
252impl SerializableSatelliteId {
253 pub const fn from_native(id: GnssSatelliteId) -> Self {
255 Self {
256 system: id.system,
257 prn: id.prn,
258 }
259 }
260
261 pub fn to_native(self) -> Result<GnssSatelliteId, FusionStateCodecError> {
263 GnssSatelliteId::new(self.system, self.prn).map_err(invalid_state)
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
269pub struct SerializableRateEndpoint {
270 pub t_j2000_s: F64Bits,
272 pub specific_force_mps2: [F64Bits; 3],
274 pub angular_rate_rps: [F64Bits; 3],
276}
277
278impl SerializableRateEndpoint {
279 fn from_native(endpoint: RateEndpoint) -> Self {
281 Self {
282 t_j2000_s: F64Bits::from_f64(endpoint.t_j2000_s),
283 specific_force_mps2: bits3(endpoint.specific_force_mps2),
284 angular_rate_rps: bits3(endpoint.angular_rate_rps),
285 }
286 }
287
288 fn to_native(self) -> RateEndpoint {
290 RateEndpoint {
291 t_j2000_s: self.t_j2000_s.to_f64(),
292 specific_force_mps2: f643(self.specific_force_mps2),
293 angular_rate_rps: f643(self.angular_rate_rps),
294 }
295 }
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
300pub enum SerializableImuSampleKind {
301 Rate {
303 specific_force_mps2: [F64Bits; 3],
305 angular_rate_rps: [F64Bits; 3],
307 },
308 Increment {
310 delta_velocity_mps: [F64Bits; 3],
312 delta_theta_rad: [F64Bits; 3],
314 dt_s: F64Bits,
316 },
317}
318
319impl SerializableImuSampleKind {
320 pub fn from_native(kind: ImuSampleKind) -> Self {
322 match kind {
323 ImuSampleKind::Rate {
324 specific_force_mps2,
325 angular_rate_rps,
326 } => Self::Rate {
327 specific_force_mps2: bits3(specific_force_mps2),
328 angular_rate_rps: bits3(angular_rate_rps),
329 },
330 ImuSampleKind::Increment {
331 delta_velocity_mps,
332 delta_theta_rad,
333 dt_s,
334 } => Self::Increment {
335 delta_velocity_mps: bits3(delta_velocity_mps),
336 delta_theta_rad: bits3(delta_theta_rad),
337 dt_s: F64Bits::from_f64(dt_s),
338 },
339 }
340 }
341
342 pub fn to_native(self) -> ImuSampleKind {
344 match self {
345 Self::Rate {
346 specific_force_mps2,
347 angular_rate_rps,
348 } => ImuSampleKind::Rate {
349 specific_force_mps2: f643(specific_force_mps2),
350 angular_rate_rps: f643(angular_rate_rps),
351 },
352 Self::Increment {
353 delta_velocity_mps,
354 delta_theta_rad,
355 dt_s,
356 } => ImuSampleKind::Increment {
357 delta_velocity_mps: f643(delta_velocity_mps),
358 delta_theta_rad: f643(delta_theta_rad),
359 dt_s: dt_s.to_f64(),
360 },
361 }
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
367pub struct SerializableImuSample {
368 pub t_j2000_s: F64Bits,
370 pub kind: SerializableImuSampleKind,
372}
373
374impl SerializableImuSample {
375 pub fn from_native(sample: ImuSample) -> Self {
377 Self {
378 t_j2000_s: F64Bits::from_f64(sample.t_j2000_s),
379 kind: SerializableImuSampleKind::from_native(sample.kind),
380 }
381 }
382
383 pub fn to_native(self) -> ImuSample {
385 ImuSample {
386 t_j2000_s: self.t_j2000_s.to_f64(),
387 kind: self.kind.to_native(),
388 }
389 }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
394pub struct SerializableStoredImuSample {
395 pub previous_t_j2000_s: F64Bits,
397 pub sample: SerializableImuSample,
399 pub previous_rate: Option<SerializableRateEndpoint>,
401}
402
403impl SerializableStoredImuSample {
404 fn from_native(sample: StoredImuSample) -> Self {
406 Self {
407 previous_t_j2000_s: F64Bits::from_f64(sample.previous_t_j2000_s),
408 sample: SerializableImuSample::from_native(sample.sample),
409 previous_rate: sample
410 .previous_rate
411 .map(SerializableRateEndpoint::from_native),
412 }
413 }
414
415 fn to_native(self) -> StoredImuSample {
417 StoredImuSample {
418 previous_t_j2000_s: self.previous_t_j2000_s.to_f64(),
419 sample: self.sample.to_native(),
420 previous_rate: self.previous_rate.map(SerializableRateEndpoint::to_native),
421 }
422 }
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
427pub struct SerializableLooseMeasurement {
428 pub t_j2000_s: F64Bits,
430 pub position_ecef_m: [F64Bits; 3],
432 pub velocity_ecef_mps: Option<[F64Bits; 3]>,
434 pub covariance: Vec<Vec<F64Bits>>,
436 pub satellites_used: u32,
438 pub solution_valid: bool,
440}
441
442impl SerializableLooseMeasurement {
443 pub fn from_native(measurement: &GnssFixMeasurement) -> Result<Self, FusionStateCodecError> {
445 Ok(Self {
446 t_j2000_s: F64Bits::from_f64(measurement.t_j2000_s),
447 position_ecef_m: bits3(measurement.position_ecef_m),
448 velocity_ecef_mps: measurement.velocity_ecef_mps.map(bits3),
449 covariance: bits_matrix(&measurement.covariance),
450 satellites_used: checked_u32(measurement.satellites_used)?,
451 solution_valid: measurement.solution_valid,
452 })
453 }
454
455 pub fn to_native(&self) -> Result<GnssFixMeasurement, FusionStateCodecError> {
457 let measurement = GnssFixMeasurement {
458 t_j2000_s: self.t_j2000_s.to_f64(),
459 position_ecef_m: f643(self.position_ecef_m),
460 velocity_ecef_mps: self.velocity_ecef_mps.map(f643),
461 covariance: f64_matrix(&self.covariance),
462 satellites_used: self.satellites_used as usize,
463 solution_valid: self.solution_valid,
464 };
465 measurement.validate().map_err(invalid_state)?;
466 Ok(measurement)
467 }
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
472pub struct SerializableTightRangeRateObservation {
473 pub measured_range_rate_m_s: F64Bits,
475 pub sigma_m_s: F64Bits,
477 pub satellite_clock_drift_m_s: F64Bits,
479}
480
481impl SerializableTightRangeRateObservation {
482 pub fn from_native(observation: TightRangeRateObservation) -> Self {
484 Self {
485 measured_range_rate_m_s: F64Bits::from_f64(observation.measured_range_rate_m_s),
486 sigma_m_s: F64Bits::from_f64(observation.sigma_m_s),
487 satellite_clock_drift_m_s: F64Bits::from_f64(observation.satellite_clock_drift_m_s),
488 }
489 }
490
491 pub fn to_native(self) -> TightRangeRateObservation {
493 TightRangeRateObservation {
494 measured_range_rate_m_s: self.measured_range_rate_m_s.to_f64(),
495 sigma_m_s: self.sigma_m_s.to_f64(),
496 satellite_clock_drift_m_s: self.satellite_clock_drift_m_s.to_f64(),
497 }
498 }
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
503pub struct SerializableTightCarrierPhaseObservation {
504 pub phase_range_m: F64Bits,
506 pub sigma_m: F64Bits,
508 pub float_ambiguity_m: F64Bits,
510}
511
512impl SerializableTightCarrierPhaseObservation {
513 pub fn from_native(observation: TightCarrierPhaseObservation) -> Self {
515 Self {
516 phase_range_m: F64Bits::from_f64(observation.phase_range_m),
517 sigma_m: F64Bits::from_f64(observation.sigma_m),
518 float_ambiguity_m: F64Bits::from_f64(observation.float_ambiguity_m),
519 }
520 }
521
522 pub fn to_native(self) -> TightCarrierPhaseObservation {
524 TightCarrierPhaseObservation {
525 phase_range_m: self.phase_range_m.to_f64(),
526 sigma_m: self.sigma_m.to_f64(),
527 float_ambiguity_m: self.float_ambiguity_m.to_f64(),
528 }
529 }
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
534pub struct SerializableTightGnssObservation {
535 pub satellite_id: SerializableSatelliteId,
537 pub pseudorange_m: F64Bits,
539 pub pseudorange_sigma_m: F64Bits,
541 pub range_rate: Option<SerializableTightRangeRateObservation>,
543 pub carrier_phase: Option<SerializableTightCarrierPhaseObservation>,
545 pub ionosphere_delay_m: F64Bits,
547 pub troposphere_delay_m: F64Bits,
549}
550
551impl SerializableTightGnssObservation {
552 pub fn from_native(observation: TightGnssObservation) -> Self {
554 Self {
555 satellite_id: SerializableSatelliteId::from_native(observation.satellite_id),
556 pseudorange_m: F64Bits::from_f64(observation.pseudorange_m),
557 pseudorange_sigma_m: F64Bits::from_f64(observation.pseudorange_sigma_m),
558 range_rate: observation
559 .range_rate
560 .map(SerializableTightRangeRateObservation::from_native),
561 carrier_phase: observation
562 .carrier_phase
563 .map(SerializableTightCarrierPhaseObservation::from_native),
564 ionosphere_delay_m: F64Bits::from_f64(observation.ionosphere_delay_m),
565 troposphere_delay_m: F64Bits::from_f64(observation.troposphere_delay_m),
566 }
567 }
568
569 pub fn to_native(self) -> Result<TightGnssObservation, FusionStateCodecError> {
571 let observation = TightGnssObservation {
572 satellite_id: self.satellite_id.to_native()?,
573 pseudorange_m: self.pseudorange_m.to_f64(),
574 pseudorange_sigma_m: self.pseudorange_sigma_m.to_f64(),
575 range_rate: self
576 .range_rate
577 .map(SerializableTightRangeRateObservation::to_native),
578 carrier_phase: self
579 .carrier_phase
580 .map(SerializableTightCarrierPhaseObservation::to_native),
581 ionosphere_delay_m: self.ionosphere_delay_m.to_f64(),
582 troposphere_delay_m: self.troposphere_delay_m.to_f64(),
583 };
584 observation.validate().map_err(invalid_state)?;
585 Ok(observation)
586 }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
591pub struct SerializableTightGnssEpoch {
592 pub t_j2000_s: F64Bits,
594 pub observations: Vec<SerializableTightGnssObservation>,
596}
597
598impl SerializableTightGnssEpoch {
599 pub fn from_native(epoch: &TightGnssEpoch) -> Self {
601 Self {
602 t_j2000_s: F64Bits::from_f64(epoch.t_j2000_s),
603 observations: epoch
604 .observations
605 .iter()
606 .copied()
607 .map(SerializableTightGnssObservation::from_native)
608 .collect(),
609 }
610 }
611
612 pub fn to_native(&self) -> Result<TightGnssEpoch, FusionStateCodecError> {
614 let observations = self
615 .observations
616 .iter()
617 .copied()
618 .map(SerializableTightGnssObservation::to_native)
619 .collect::<Result<Vec<_>, _>>()?;
620 let epoch = TightGnssEpoch {
621 t_j2000_s: self.t_j2000_s.to_f64(),
622 observations,
623 };
624 epoch.validate().map_err(invalid_state)?;
625 Ok(epoch)
626 }
627}
628
629#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
631pub enum SerializableStoredGnssMeasurement {
632 Loose(SerializableLooseMeasurement),
634 Tight(SerializableTightGnssEpoch),
636}
637
638impl SerializableStoredGnssMeasurement {
639 fn from_native(measurement: &StoredGnssMeasurement) -> Result<Self, FusionStateCodecError> {
641 match measurement {
642 StoredGnssMeasurement::Loose(measurement) => Ok(Self::Loose(
643 SerializableLooseMeasurement::from_native(measurement)?,
644 )),
645 StoredGnssMeasurement::Tight(epoch) => {
646 Ok(Self::Tight(SerializableTightGnssEpoch::from_native(epoch)))
647 }
648 }
649 }
650
651 fn to_native(&self) -> Result<StoredGnssMeasurement, FusionStateCodecError> {
653 match self {
654 Self::Loose(measurement) => Ok(StoredGnssMeasurement::Loose(measurement.to_native()?)),
655 Self::Tight(epoch) => Ok(StoredGnssMeasurement::Tight(epoch.to_native()?)),
656 }
657 }
658}
659
660#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
662pub struct SerializableStoredCheckpoint {
663 pub t_j2000_s: F64Bits,
665 pub snapshot: Box<SerializableFusionSnapshot>,
667}
668
669impl SerializableStoredCheckpoint {
670 fn from_native(checkpoint: &StoredCheckpoint) -> Self {
672 Self {
673 t_j2000_s: F64Bits::from_f64(checkpoint.t_j2000_s),
674 snapshot: Box::new(SerializableFusionSnapshot::from_snapshot(
675 &checkpoint.snapshot,
676 )),
677 }
678 }
679
680 fn to_native(&self) -> Result<StoredCheckpoint, FusionStateCodecError> {
682 let snapshot = self.snapshot.to_snapshot()?;
683 let checkpoint = StoredCheckpoint {
684 t_j2000_s: self.t_j2000_s.to_f64(),
685 snapshot,
686 };
687 if checkpoint.t_j2000_s != checkpoint.snapshot.state.nominal.t_j2000_s {
688 return Err(FusionStateCodecError::InvalidState {
689 reason: "checkpoint epoch must match snapshot".to_string(),
690 });
691 }
692 Ok(checkpoint)
693 }
694}
695
696#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
698pub struct SerializableFusionSnapshot {
699 pub state: SerializableInsFilterState,
701 pub last_body_rate_wrt_ecef_rps: [F64Bits; 3],
703 pub tight: SerializableTightFilterState,
705}
706
707impl SerializableFusionSnapshot {
708 pub fn from_snapshot(snapshot: &InertialFilterSnapshot) -> Self {
710 Self {
711 state: SerializableInsFilterState::from_native(&snapshot.state),
712 last_body_rate_wrt_ecef_rps: bits3(snapshot.last_body_rate_wrt_ecef_rps),
713 tight: SerializableTightFilterState::from_native(&snapshot.tight),
714 }
715 }
716
717 pub fn to_snapshot(&self) -> Result<InertialFilterSnapshot, FusionStateCodecError> {
719 let state = self.state.to_native()?;
720 let last_body_rate_wrt_ecef_rps = f643(self.last_body_rate_wrt_ecef_rps);
721 validate_finite_slice(&last_body_rate_wrt_ecef_rps, "last_body_rate_wrt_ecef_rps")
722 .map_err(invalid_state)?;
723 let tight = self.tight.to_native(state.dimension())?;
724 Ok(InertialFilterSnapshot {
725 state,
726 last_body_rate_wrt_ecef_rps,
727 tight,
728 })
729 }
730}
731
732#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
734pub struct SerializableTimeSyncHistory {
735 pub config: SerializableTimeSyncHistoryConfig,
737 pub imu_samples: Vec<SerializableStoredImuSample>,
739 pub checkpoints: Vec<SerializableStoredCheckpoint>,
741 pub measurements: Vec<SerializableStoredGnssMeasurement>,
743}
744
745impl SerializableTimeSyncHistory {
746 fn from_native(history: &TimeSyncHistorySnapshot) -> Result<Self, FusionStateCodecError> {
748 Ok(Self {
749 config: SerializableTimeSyncHistoryConfig::from_native(history.config)?,
750 imu_samples: history
751 .imu_samples
752 .iter()
753 .copied()
754 .map(SerializableStoredImuSample::from_native)
755 .collect(),
756 checkpoints: history
757 .checkpoints
758 .iter()
759 .map(SerializableStoredCheckpoint::from_native)
760 .collect(),
761 measurements: history
762 .measurements
763 .iter()
764 .map(SerializableStoredGnssMeasurement::from_native)
765 .collect::<Result<Vec<_>, _>>()?,
766 })
767 }
768
769 fn to_native(&self) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
771 let snapshot = TimeSyncHistorySnapshot {
772 config: self.config.to_native()?,
773 imu_samples: self
774 .imu_samples
775 .iter()
776 .copied()
777 .map(SerializableStoredImuSample::to_native)
778 .collect(),
779 checkpoints: self
780 .checkpoints
781 .iter()
782 .map(SerializableStoredCheckpoint::to_native)
783 .collect::<Result<Vec<_>, _>>()?,
784 measurements: self
785 .measurements
786 .iter()
787 .map(SerializableStoredGnssMeasurement::to_native)
788 .collect::<Result<Vec<_>, _>>()?,
789 };
790 validate_history_by_restore(snapshot)
791 }
792}
793
794#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
796pub struct SerializableFusionState {
797 pub version: u16,
799 pub state: SerializableInsFilterState,
801 pub last_body_rate_wrt_ecef_rps: [F64Bits; 3],
803 pub tight: SerializableTightFilterState,
805 pub time_sync: SerializableTimeSyncHistory,
807}
808
809impl SerializableFusionState {
810 pub fn from_snapshot(snapshot: &InertialFilterSnapshot) -> Self {
812 let history = TimeSyncHistorySnapshot::from_filter_snapshot(snapshot.clone());
813 let time_sync =
814 SerializableTimeSyncHistory::from_native(&history).expect("default history encodes");
815 Self {
816 version: FUSION_STATE_CODEC_VERSION,
817 state: SerializableInsFilterState::from_native(&snapshot.state),
818 last_body_rate_wrt_ecef_rps: bits3(snapshot.last_body_rate_wrt_ecef_rps),
819 tight: SerializableTightFilterState::from_native(&snapshot.tight),
820 time_sync,
821 }
822 }
823
824 pub fn from_filter(filter: &InertialFilter) -> Result<Self, FusionStateCodecError> {
826 let snapshot = filter.snapshot();
827 Ok(Self {
828 version: FUSION_STATE_CODEC_VERSION,
829 state: SerializableInsFilterState::from_native(&snapshot.state),
830 last_body_rate_wrt_ecef_rps: bits3(snapshot.last_body_rate_wrt_ecef_rps),
831 tight: SerializableTightFilterState::from_native(&snapshot.tight),
832 time_sync: SerializableTimeSyncHistory::from_native(
833 &filter.time_sync.snapshot_history(),
834 )?,
835 })
836 }
837
838 pub fn to_snapshot(&self) -> Result<InertialFilterSnapshot, FusionStateCodecError> {
840 self.validate_version()?;
841 let state = self.state.to_native()?;
842 let last_body_rate_wrt_ecef_rps = f643(self.last_body_rate_wrt_ecef_rps);
843 validate_finite_slice(&last_body_rate_wrt_ecef_rps, "last_body_rate_wrt_ecef_rps")
844 .map_err(invalid_state)?;
845 let tight = self.tight.to_native(state.dimension())?;
846 Ok(InertialFilterSnapshot {
847 state,
848 last_body_rate_wrt_ecef_rps,
849 tight,
850 })
851 }
852
853 fn to_time_sync_history(&self) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
855 self.validate_version()?;
856 self.time_sync.to_native()
857 }
858
859 pub fn encode_versioned(&self) -> Result<Vec<u8>, FusionStateCodecError> {
861 self.validate_version()?;
862 let mut bytes = Vec::new();
863 bytes.extend_from_slice(&FUSION_STATE_MAGIC);
864 write_u16(&mut bytes, self.version);
865 write_layout(&mut bytes, self.state.layout);
866 write_nav(&mut bytes, &self.state.nominal);
867 write_f64_vec(&mut bytes, &self.state.error_state)?;
868 write_f64_matrix(&mut bytes, &self.state.covariance)?;
869 write_f64_array(&mut bytes, &self.state.accel_scale_factor);
870 write_f64_array(&mut bytes, &self.state.gyro_scale_factor);
871 write_f64_array(&mut bytes, &self.last_body_rate_wrt_ecef_rps);
872 write_f64(&mut bytes, self.tight.clock_bias_m);
873 write_f64(&mut bytes, self.tight.clock_drift_m_s);
874 write_f64_matrix(&mut bytes, &self.tight.augmented_covariance)?;
875 write_time_sync_history(&mut bytes, &self.time_sync)?;
876 let checksum = fnv1a64(&bytes);
877 write_u64(&mut bytes, checksum);
878 Ok(bytes)
879 }
880
881 pub fn decode_versioned(bytes: &[u8]) -> Result<Self, FusionStateCodecError> {
883 let minimum = FUSION_STATE_MAGIC.len() + 2 + 8;
884 if bytes.len() < minimum {
885 return Err(FusionStateCodecError::Truncated {
886 offset: 0,
887 needed: minimum,
888 actual: bytes.len(),
889 });
890 }
891 if bytes[..FUSION_STATE_MAGIC.len()] != FUSION_STATE_MAGIC {
892 return Err(FusionStateCodecError::InvalidMagic);
893 }
894 let checksum_offset = bytes.len() - 8;
895 let expected = read_u64_at(bytes, checksum_offset)?;
896 let found = fnv1a64(&bytes[..checksum_offset]);
897 if expected != found {
898 return Err(FusionStateCodecError::Checksum { expected, found });
899 }
900
901 let mut cursor = FUSION_STATE_MAGIC.len();
902 let version = read_u16(bytes, &mut cursor, checksum_offset)?;
903 if version != FUSION_STATE_CODEC_VERSION {
904 return Err(FusionStateCodecError::UnsupportedVersion { version });
905 }
906 let layout = read_layout(bytes, &mut cursor, checksum_offset)?;
907 let nominal = read_nav(bytes, &mut cursor, checksum_offset)?;
908 let error_state = read_f64_vec(bytes, &mut cursor, checksum_offset)?;
909 let covariance = read_f64_matrix(bytes, &mut cursor, checksum_offset)?;
910 let accel_scale_factor = read_f64_array(bytes, &mut cursor, checksum_offset)?;
911 let gyro_scale_factor = read_f64_array(bytes, &mut cursor, checksum_offset)?;
912 let last_body_rate_wrt_ecef_rps = read_f64_array(bytes, &mut cursor, checksum_offset)?;
913 let clock_bias_m = read_f64(bytes, &mut cursor, checksum_offset)?;
914 let clock_drift_m_s = read_f64(bytes, &mut cursor, checksum_offset)?;
915 let augmented_covariance = read_f64_matrix(bytes, &mut cursor, checksum_offset)?;
916 let time_sync = read_time_sync_history(bytes, &mut cursor, checksum_offset)?;
917 if cursor != checksum_offset {
918 return Err(FusionStateCodecError::TrailingBytes {
919 remaining: checksum_offset - cursor,
920 });
921 }
922 let state = Self {
923 version,
924 state: SerializableInsFilterState {
925 layout,
926 nominal,
927 error_state,
928 covariance,
929 accel_scale_factor,
930 gyro_scale_factor,
931 },
932 last_body_rate_wrt_ecef_rps,
933 tight: SerializableTightFilterState {
934 clock_bias_m,
935 clock_drift_m_s,
936 augmented_covariance,
937 },
938 time_sync,
939 };
940 state.to_snapshot()?;
941 state.to_time_sync_history()?;
942 Ok(state)
943 }
944
945 pub fn to_json_string(&self) -> Result<String, FusionStateCodecError> {
947 self.validate_version()?;
948 serde_json::to_string(self).map_err(|error| FusionStateCodecError::Json {
949 message: error.to_string(),
950 })
951 }
952
953 pub fn from_json_str(text: &str) -> Result<Self, FusionStateCodecError> {
955 let state: Self =
956 serde_json::from_str(text).map_err(|error| FusionStateCodecError::Json {
957 message: error.to_string(),
958 })?;
959 state.to_snapshot()?;
960 state.to_time_sync_history()?;
961 Ok(state)
962 }
963
964 fn validate_version(&self) -> Result<(), FusionStateCodecError> {
965 if self.version == FUSION_STATE_CODEC_VERSION {
966 Ok(())
967 } else {
968 Err(FusionStateCodecError::UnsupportedVersion {
969 version: self.version,
970 })
971 }
972 }
973}
974
975impl InertialFilterSnapshot {
976 pub fn to_serializable_fusion_state(&self) -> SerializableFusionState {
978 SerializableFusionState::from_snapshot(self)
979 }
980
981 pub fn encode_fusion_state(&self) -> Result<Vec<u8>, FusionStateCodecError> {
983 self.to_serializable_fusion_state().encode_versioned()
984 }
985
986 pub fn decode_fusion_state(bytes: &[u8]) -> Result<Self, FusionStateCodecError> {
988 SerializableFusionState::decode_versioned(bytes)?.to_snapshot()
989 }
990}
991
992impl InertialFilter {
993 pub fn serializable_state(&self) -> Result<SerializableFusionState, FusionStateCodecError> {
995 SerializableFusionState::from_filter(self)
996 }
997
998 pub fn encode_state(&self) -> Result<Vec<u8>, FusionStateCodecError> {
1000 SerializableFusionState::from_filter(self)?.encode_versioned()
1001 }
1002
1003 pub fn restore_serializable_state(
1005 &mut self,
1006 state: &SerializableFusionState,
1007 ) -> Result<(), FusionStateCodecError> {
1008 let snapshot = state.to_snapshot()?;
1009 let history = state.to_time_sync_history()?;
1010 self.restore_snapshot(&snapshot).map_err(invalid_state)?;
1011 self.time_sync
1012 .restore_history(history)
1013 .map_err(invalid_state)
1014 }
1015
1016 pub fn restore_encoded_state(&mut self, bytes: &[u8]) -> Result<(), FusionStateCodecError> {
1018 let state = SerializableFusionState::decode_versioned(bytes)?;
1019 self.restore_serializable_state(&state)
1020 }
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1025pub enum FusionStateCodecError {
1026 #[error("fusion state payload has invalid magic")]
1028 InvalidMagic,
1029 #[error("fusion state version {version} is not supported")]
1031 UnsupportedVersion {
1032 version: u16,
1034 },
1035 #[error("fusion state payload truncated at {offset}, needed {needed} bytes, got {actual}")]
1037 Truncated {
1038 offset: usize,
1040 needed: usize,
1042 actual: usize,
1044 },
1045 #[error("fusion state checksum expected {expected:#x} but found {found:#x}")]
1047 Checksum {
1048 expected: u64,
1050 found: u64,
1052 },
1053 #[error("fusion state payload has {remaining} trailing bytes")]
1055 TrailingBytes {
1056 remaining: usize,
1058 },
1059 #[error("invalid fusion state payload: {reason}")]
1061 InvalidState {
1062 reason: String,
1064 },
1065 #[error("fusion state JSON error: {message}")]
1067 Json {
1068 message: String,
1070 },
1071}
1072
1073fn invalid_state(error: impl core::fmt::Display) -> FusionStateCodecError {
1074 FusionStateCodecError::InvalidState {
1075 reason: error.to_string(),
1076 }
1077}
1078
1079fn checked_u32(value: usize) -> Result<u32, FusionStateCodecError> {
1080 u32::try_from(value).map_err(|_| FusionStateCodecError::InvalidState {
1081 reason: "length exceeds u32".to_string(),
1082 })
1083}
1084
1085fn validate_history_by_restore(
1086 snapshot: TimeSyncHistorySnapshot,
1087) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
1088 snapshot.validate().map_err(invalid_state)?;
1089 Ok(snapshot)
1090}
1091
1092fn bits3(values: [f64; 3]) -> [F64Bits; 3] {
1093 values.map(F64Bits::from_f64)
1094}
1095
1096fn f643(values: [F64Bits; 3]) -> [f64; 3] {
1097 values.map(F64Bits::to_f64)
1098}
1099
1100fn bits3x3(values: [[f64; 3]; 3]) -> [[F64Bits; 3]; 3] {
1101 values.map(bits3)
1102}
1103
1104fn f643x3(values: [[F64Bits; 3]; 3]) -> [[f64; 3]; 3] {
1105 values.map(f643)
1106}
1107
1108fn bits_slice(values: &[f64]) -> Vec<F64Bits> {
1109 values.iter().copied().map(F64Bits::from_f64).collect()
1110}
1111
1112fn f64_vec(values: &[F64Bits]) -> Vec<f64> {
1113 values.iter().copied().map(F64Bits::to_f64).collect()
1114}
1115
1116fn bits_matrix(values: &[Vec<f64>]) -> Vec<Vec<F64Bits>> {
1117 values.iter().map(|row| bits_slice(row)).collect()
1118}
1119
1120fn f64_matrix(values: &[Vec<F64Bits>]) -> Vec<Vec<f64>> {
1121 values.iter().map(|row| f64_vec(row)).collect()
1122}
1123
1124fn write_nav(bytes: &mut Vec<u8>, state: &SerializableNavState) {
1125 write_f64(bytes, state.t_j2000_s);
1126 write_f64_array(bytes, &state.position_ecef_m);
1127 write_f64_array(bytes, &state.velocity_ecef_mps);
1128 for row in &state.attitude_body_to_ecef {
1129 write_f64_array(bytes, row);
1130 }
1131 write_f64_array(bytes, &state.accel_bias_mps2);
1132 write_f64_array(bytes, &state.gyro_bias_rps);
1133}
1134
1135fn read_nav(
1136 bytes: &[u8],
1137 cursor: &mut usize,
1138 limit: usize,
1139) -> Result<SerializableNavState, FusionStateCodecError> {
1140 Ok(SerializableNavState {
1141 t_j2000_s: read_f64(bytes, cursor, limit)?,
1142 position_ecef_m: read_f64_array(bytes, cursor, limit)?,
1143 velocity_ecef_mps: read_f64_array(bytes, cursor, limit)?,
1144 attitude_body_to_ecef: [
1145 read_f64_array(bytes, cursor, limit)?,
1146 read_f64_array(bytes, cursor, limit)?,
1147 read_f64_array(bytes, cursor, limit)?,
1148 ],
1149 accel_bias_mps2: read_f64_array(bytes, cursor, limit)?,
1150 gyro_bias_rps: read_f64_array(bytes, cursor, limit)?,
1151 })
1152}
1153
1154fn write_layout(bytes: &mut Vec<u8>, layout: SerializableErrorStateLayout) {
1155 bytes.push(match layout {
1156 SerializableErrorStateLayout::Fifteen => 15,
1157 SerializableErrorStateLayout::TwentyOne => 21,
1158 });
1159}
1160
1161fn read_layout(
1162 bytes: &[u8],
1163 cursor: &mut usize,
1164 limit: usize,
1165) -> Result<SerializableErrorStateLayout, FusionStateCodecError> {
1166 match read_u8(bytes, cursor, limit)? {
1167 15 => Ok(SerializableErrorStateLayout::Fifteen),
1168 21 => Ok(SerializableErrorStateLayout::TwentyOne),
1169 _ => Err(FusionStateCodecError::InvalidState {
1170 reason: "invalid error-state layout tag".to_string(),
1171 }),
1172 }
1173}
1174
1175fn write_time_sync_history(
1176 bytes: &mut Vec<u8>,
1177 history: &SerializableTimeSyncHistory,
1178) -> Result<(), FusionStateCodecError> {
1179 write_u32_checked(bytes, history.config.imu_capacity as usize)?;
1180 write_u32_checked(bytes, history.config.checkpoint_capacity as usize)?;
1181 write_u32_checked(bytes, history.imu_samples.len())?;
1182 for sample in &history.imu_samples {
1183 write_stored_imu_sample(bytes, sample);
1184 }
1185 write_u32_checked(bytes, history.checkpoints.len())?;
1186 for checkpoint in &history.checkpoints {
1187 write_stored_checkpoint(bytes, checkpoint)?;
1188 }
1189 write_u32_checked(bytes, history.measurements.len())?;
1190 for measurement in &history.measurements {
1191 write_stored_measurement(bytes, measurement)?;
1192 }
1193 Ok(())
1194}
1195
1196fn read_time_sync_history(
1197 bytes: &[u8],
1198 cursor: &mut usize,
1199 limit: usize,
1200) -> Result<SerializableTimeSyncHistory, FusionStateCodecError> {
1201 let config = SerializableTimeSyncHistoryConfig {
1202 imu_capacity: read_u32(bytes, cursor, limit)?,
1203 checkpoint_capacity: read_u32(bytes, cursor, limit)?,
1204 };
1205 let imu_len = read_len(bytes, cursor, limit, 1, "imu_samples")?;
1206 let mut imu_samples = Vec::with_capacity(imu_len);
1207 for _ in 0..imu_len {
1208 imu_samples.push(read_stored_imu_sample(bytes, cursor, limit)?);
1209 }
1210 let checkpoint_len = read_len(bytes, cursor, limit, 1, "checkpoints")?;
1211 let mut checkpoints = Vec::with_capacity(checkpoint_len);
1212 for _ in 0..checkpoint_len {
1213 checkpoints.push(read_stored_checkpoint(bytes, cursor, limit)?);
1214 }
1215 let measurement_len = read_len(bytes, cursor, limit, 1, "gnss_measurements")?;
1216 let mut measurements = Vec::with_capacity(measurement_len);
1217 for _ in 0..measurement_len {
1218 measurements.push(read_stored_measurement(bytes, cursor, limit)?);
1219 }
1220 let history = SerializableTimeSyncHistory {
1221 config,
1222 imu_samples,
1223 checkpoints,
1224 measurements,
1225 };
1226 history.to_native()?;
1227 Ok(history)
1228}
1229
1230fn write_stored_imu_sample(bytes: &mut Vec<u8>, sample: &SerializableStoredImuSample) {
1231 write_f64(bytes, sample.previous_t_j2000_s);
1232 write_imu_sample(bytes, &sample.sample);
1233 match sample.previous_rate {
1234 Some(endpoint) => {
1235 write_bool(bytes, true);
1236 write_rate_endpoint(bytes, endpoint);
1237 }
1238 None => write_bool(bytes, false),
1239 }
1240}
1241
1242fn read_stored_imu_sample(
1243 bytes: &[u8],
1244 cursor: &mut usize,
1245 limit: usize,
1246) -> Result<SerializableStoredImuSample, FusionStateCodecError> {
1247 let previous_t_j2000_s = read_f64(bytes, cursor, limit)?;
1248 let sample = read_imu_sample(bytes, cursor, limit)?;
1249 let previous_rate = if read_bool(bytes, cursor, limit)? {
1250 Some(read_rate_endpoint(bytes, cursor, limit)?)
1251 } else {
1252 None
1253 };
1254 Ok(SerializableStoredImuSample {
1255 previous_t_j2000_s,
1256 sample,
1257 previous_rate,
1258 })
1259}
1260
1261fn write_imu_sample(bytes: &mut Vec<u8>, sample: &SerializableImuSample) {
1262 write_f64(bytes, sample.t_j2000_s);
1263 match sample.kind {
1264 SerializableImuSampleKind::Rate {
1265 specific_force_mps2,
1266 angular_rate_rps,
1267 } => {
1268 bytes.push(0);
1269 write_f64_array(bytes, &specific_force_mps2);
1270 write_f64_array(bytes, &angular_rate_rps);
1271 }
1272 SerializableImuSampleKind::Increment {
1273 delta_velocity_mps,
1274 delta_theta_rad,
1275 dt_s,
1276 } => {
1277 bytes.push(1);
1278 write_f64_array(bytes, &delta_velocity_mps);
1279 write_f64_array(bytes, &delta_theta_rad);
1280 write_f64(bytes, dt_s);
1281 }
1282 }
1283}
1284
1285fn read_imu_sample(
1286 bytes: &[u8],
1287 cursor: &mut usize,
1288 limit: usize,
1289) -> Result<SerializableImuSample, FusionStateCodecError> {
1290 let t_j2000_s = read_f64(bytes, cursor, limit)?;
1291 let kind = match read_u8(bytes, cursor, limit)? {
1292 0 => SerializableImuSampleKind::Rate {
1293 specific_force_mps2: read_f64_array(bytes, cursor, limit)?,
1294 angular_rate_rps: read_f64_array(bytes, cursor, limit)?,
1295 },
1296 1 => SerializableImuSampleKind::Increment {
1297 delta_velocity_mps: read_f64_array(bytes, cursor, limit)?,
1298 delta_theta_rad: read_f64_array(bytes, cursor, limit)?,
1299 dt_s: read_f64(bytes, cursor, limit)?,
1300 },
1301 _ => {
1302 return Err(FusionStateCodecError::InvalidState {
1303 reason: "invalid IMU sample kind tag".to_string(),
1304 });
1305 }
1306 };
1307 Ok(SerializableImuSample { t_j2000_s, kind })
1308}
1309
1310fn write_rate_endpoint(bytes: &mut Vec<u8>, endpoint: SerializableRateEndpoint) {
1311 write_f64(bytes, endpoint.t_j2000_s);
1312 write_f64_array(bytes, &endpoint.specific_force_mps2);
1313 write_f64_array(bytes, &endpoint.angular_rate_rps);
1314}
1315
1316fn read_rate_endpoint(
1317 bytes: &[u8],
1318 cursor: &mut usize,
1319 limit: usize,
1320) -> Result<SerializableRateEndpoint, FusionStateCodecError> {
1321 Ok(SerializableRateEndpoint {
1322 t_j2000_s: read_f64(bytes, cursor, limit)?,
1323 specific_force_mps2: read_f64_array(bytes, cursor, limit)?,
1324 angular_rate_rps: read_f64_array(bytes, cursor, limit)?,
1325 })
1326}
1327
1328fn write_stored_checkpoint(
1329 bytes: &mut Vec<u8>,
1330 checkpoint: &SerializableStoredCheckpoint,
1331) -> Result<(), FusionStateCodecError> {
1332 write_f64(bytes, checkpoint.t_j2000_s);
1333 write_fusion_snapshot(bytes, checkpoint.snapshot.as_ref())
1334}
1335
1336fn read_stored_checkpoint(
1337 bytes: &[u8],
1338 cursor: &mut usize,
1339 limit: usize,
1340) -> Result<SerializableStoredCheckpoint, FusionStateCodecError> {
1341 Ok(SerializableStoredCheckpoint {
1342 t_j2000_s: read_f64(bytes, cursor, limit)?,
1343 snapshot: Box::new(read_fusion_snapshot(bytes, cursor, limit)?),
1344 })
1345}
1346
1347fn write_fusion_snapshot(
1348 bytes: &mut Vec<u8>,
1349 snapshot: &SerializableFusionSnapshot,
1350) -> Result<(), FusionStateCodecError> {
1351 write_layout(bytes, snapshot.state.layout);
1352 write_nav(bytes, &snapshot.state.nominal);
1353 write_f64_vec(bytes, &snapshot.state.error_state)?;
1354 write_f64_matrix(bytes, &snapshot.state.covariance)?;
1355 write_f64_array(bytes, &snapshot.state.accel_scale_factor);
1356 write_f64_array(bytes, &snapshot.state.gyro_scale_factor);
1357 write_f64_array(bytes, &snapshot.last_body_rate_wrt_ecef_rps);
1358 write_f64(bytes, snapshot.tight.clock_bias_m);
1359 write_f64(bytes, snapshot.tight.clock_drift_m_s);
1360 write_f64_matrix(bytes, &snapshot.tight.augmented_covariance)
1361}
1362
1363fn read_fusion_snapshot(
1364 bytes: &[u8],
1365 cursor: &mut usize,
1366 limit: usize,
1367) -> Result<SerializableFusionSnapshot, FusionStateCodecError> {
1368 Ok(SerializableFusionSnapshot {
1369 state: SerializableInsFilterState {
1370 layout: read_layout(bytes, cursor, limit)?,
1371 nominal: read_nav(bytes, cursor, limit)?,
1372 error_state: read_f64_vec(bytes, cursor, limit)?,
1373 covariance: read_f64_matrix(bytes, cursor, limit)?,
1374 accel_scale_factor: read_f64_array(bytes, cursor, limit)?,
1375 gyro_scale_factor: read_f64_array(bytes, cursor, limit)?,
1376 },
1377 last_body_rate_wrt_ecef_rps: read_f64_array(bytes, cursor, limit)?,
1378 tight: SerializableTightFilterState {
1379 clock_bias_m: read_f64(bytes, cursor, limit)?,
1380 clock_drift_m_s: read_f64(bytes, cursor, limit)?,
1381 augmented_covariance: read_f64_matrix(bytes, cursor, limit)?,
1382 },
1383 })
1384}
1385
1386fn write_stored_measurement(
1387 bytes: &mut Vec<u8>,
1388 measurement: &SerializableStoredGnssMeasurement,
1389) -> Result<(), FusionStateCodecError> {
1390 match measurement {
1391 SerializableStoredGnssMeasurement::Loose(measurement) => {
1392 bytes.push(0);
1393 write_loose_measurement(bytes, measurement)
1394 }
1395 SerializableStoredGnssMeasurement::Tight(epoch) => {
1396 bytes.push(1);
1397 write_tight_epoch(bytes, epoch)
1398 }
1399 }
1400}
1401
1402fn read_stored_measurement(
1403 bytes: &[u8],
1404 cursor: &mut usize,
1405 limit: usize,
1406) -> Result<SerializableStoredGnssMeasurement, FusionStateCodecError> {
1407 match read_u8(bytes, cursor, limit)? {
1408 0 => Ok(SerializableStoredGnssMeasurement::Loose(
1409 read_loose_measurement(bytes, cursor, limit)?,
1410 )),
1411 1 => Ok(SerializableStoredGnssMeasurement::Tight(read_tight_epoch(
1412 bytes, cursor, limit,
1413 )?)),
1414 _ => Err(FusionStateCodecError::InvalidState {
1415 reason: "invalid GNSS measurement tag".to_string(),
1416 }),
1417 }
1418}
1419
1420fn write_loose_measurement(
1421 bytes: &mut Vec<u8>,
1422 measurement: &SerializableLooseMeasurement,
1423) -> Result<(), FusionStateCodecError> {
1424 write_f64(bytes, measurement.t_j2000_s);
1425 write_f64_array(bytes, &measurement.position_ecef_m);
1426 match measurement.velocity_ecef_mps {
1427 Some(velocity) => {
1428 write_bool(bytes, true);
1429 write_f64_array(bytes, &velocity);
1430 }
1431 None => write_bool(bytes, false),
1432 }
1433 write_f64_matrix(bytes, &measurement.covariance)?;
1434 write_u32_checked(bytes, measurement.satellites_used as usize)?;
1435 write_bool(bytes, measurement.solution_valid);
1436 Ok(())
1437}
1438
1439fn read_loose_measurement(
1440 bytes: &[u8],
1441 cursor: &mut usize,
1442 limit: usize,
1443) -> Result<SerializableLooseMeasurement, FusionStateCodecError> {
1444 let t_j2000_s = read_f64(bytes, cursor, limit)?;
1445 let position_ecef_m = read_f64_array(bytes, cursor, limit)?;
1446 let velocity_ecef_mps = if read_bool(bytes, cursor, limit)? {
1447 Some(read_f64_array(bytes, cursor, limit)?)
1448 } else {
1449 None
1450 };
1451 Ok(SerializableLooseMeasurement {
1452 t_j2000_s,
1453 position_ecef_m,
1454 velocity_ecef_mps,
1455 covariance: read_f64_matrix(bytes, cursor, limit)?,
1456 satellites_used: read_u32(bytes, cursor, limit)?,
1457 solution_valid: read_bool(bytes, cursor, limit)?,
1458 })
1459}
1460
1461fn write_tight_epoch(
1462 bytes: &mut Vec<u8>,
1463 epoch: &SerializableTightGnssEpoch,
1464) -> Result<(), FusionStateCodecError> {
1465 write_f64(bytes, epoch.t_j2000_s);
1466 write_u32_checked(bytes, epoch.observations.len())?;
1467 for observation in &epoch.observations {
1468 write_tight_observation(bytes, observation);
1469 }
1470 Ok(())
1471}
1472
1473fn read_tight_epoch(
1474 bytes: &[u8],
1475 cursor: &mut usize,
1476 limit: usize,
1477) -> Result<SerializableTightGnssEpoch, FusionStateCodecError> {
1478 let t_j2000_s = read_f64(bytes, cursor, limit)?;
1479 let len = read_len(bytes, cursor, limit, 1, "tight_observations")?;
1480 let mut observations = Vec::with_capacity(len);
1481 for _ in 0..len {
1482 observations.push(read_tight_observation(bytes, cursor, limit)?);
1483 }
1484 Ok(SerializableTightGnssEpoch {
1485 t_j2000_s,
1486 observations,
1487 })
1488}
1489
1490fn write_tight_observation(bytes: &mut Vec<u8>, observation: &SerializableTightGnssObservation) {
1491 write_satellite_id(bytes, observation.satellite_id);
1492 write_f64(bytes, observation.pseudorange_m);
1493 write_f64(bytes, observation.pseudorange_sigma_m);
1494 match observation.range_rate {
1495 Some(range_rate) => {
1496 write_bool(bytes, true);
1497 write_range_rate_observation(bytes, range_rate);
1498 }
1499 None => write_bool(bytes, false),
1500 }
1501 match observation.carrier_phase {
1502 Some(carrier_phase) => {
1503 write_bool(bytes, true);
1504 write_carrier_phase_observation(bytes, carrier_phase);
1505 }
1506 None => write_bool(bytes, false),
1507 }
1508 write_f64(bytes, observation.ionosphere_delay_m);
1509 write_f64(bytes, observation.troposphere_delay_m);
1510}
1511
1512fn read_tight_observation(
1513 bytes: &[u8],
1514 cursor: &mut usize,
1515 limit: usize,
1516) -> Result<SerializableTightGnssObservation, FusionStateCodecError> {
1517 let satellite_id = read_satellite_id(bytes, cursor, limit)?;
1518 let pseudorange_m = read_f64(bytes, cursor, limit)?;
1519 let pseudorange_sigma_m = read_f64(bytes, cursor, limit)?;
1520 let range_rate = if read_bool(bytes, cursor, limit)? {
1521 Some(read_range_rate_observation(bytes, cursor, limit)?)
1522 } else {
1523 None
1524 };
1525 let carrier_phase = if read_bool(bytes, cursor, limit)? {
1526 Some(read_carrier_phase_observation(bytes, cursor, limit)?)
1527 } else {
1528 None
1529 };
1530 Ok(SerializableTightGnssObservation {
1531 satellite_id,
1532 pseudorange_m,
1533 pseudorange_sigma_m,
1534 range_rate,
1535 carrier_phase,
1536 ionosphere_delay_m: read_f64(bytes, cursor, limit)?,
1537 troposphere_delay_m: read_f64(bytes, cursor, limit)?,
1538 })
1539}
1540
1541fn write_range_rate_observation(
1542 bytes: &mut Vec<u8>,
1543 observation: SerializableTightRangeRateObservation,
1544) {
1545 write_f64(bytes, observation.measured_range_rate_m_s);
1546 write_f64(bytes, observation.sigma_m_s);
1547 write_f64(bytes, observation.satellite_clock_drift_m_s);
1548}
1549
1550fn read_range_rate_observation(
1551 bytes: &[u8],
1552 cursor: &mut usize,
1553 limit: usize,
1554) -> Result<SerializableTightRangeRateObservation, FusionStateCodecError> {
1555 Ok(SerializableTightRangeRateObservation {
1556 measured_range_rate_m_s: read_f64(bytes, cursor, limit)?,
1557 sigma_m_s: read_f64(bytes, cursor, limit)?,
1558 satellite_clock_drift_m_s: read_f64(bytes, cursor, limit)?,
1559 })
1560}
1561
1562fn write_carrier_phase_observation(
1563 bytes: &mut Vec<u8>,
1564 observation: SerializableTightCarrierPhaseObservation,
1565) {
1566 write_f64(bytes, observation.phase_range_m);
1567 write_f64(bytes, observation.sigma_m);
1568 write_f64(bytes, observation.float_ambiguity_m);
1569}
1570
1571fn read_carrier_phase_observation(
1572 bytes: &[u8],
1573 cursor: &mut usize,
1574 limit: usize,
1575) -> Result<SerializableTightCarrierPhaseObservation, FusionStateCodecError> {
1576 Ok(SerializableTightCarrierPhaseObservation {
1577 phase_range_m: read_f64(bytes, cursor, limit)?,
1578 sigma_m: read_f64(bytes, cursor, limit)?,
1579 float_ambiguity_m: read_f64(bytes, cursor, limit)?,
1580 })
1581}
1582
1583fn write_satellite_id(bytes: &mut Vec<u8>, id: SerializableSatelliteId) {
1584 bytes.push(match id.system {
1585 GnssSystem::Gps => 0,
1586 GnssSystem::Glonass => 1,
1587 GnssSystem::Galileo => 2,
1588 GnssSystem::BeiDou => 3,
1589 GnssSystem::Qzss => 4,
1590 GnssSystem::Navic => 5,
1591 GnssSystem::Sbas => 6,
1592 });
1593 bytes.push(id.prn);
1594}
1595
1596fn read_satellite_id(
1597 bytes: &[u8],
1598 cursor: &mut usize,
1599 limit: usize,
1600) -> Result<SerializableSatelliteId, FusionStateCodecError> {
1601 let system = match read_u8(bytes, cursor, limit)? {
1602 0 => GnssSystem::Gps,
1603 1 => GnssSystem::Glonass,
1604 2 => GnssSystem::Galileo,
1605 3 => GnssSystem::BeiDou,
1606 4 => GnssSystem::Qzss,
1607 5 => GnssSystem::Navic,
1608 6 => GnssSystem::Sbas,
1609 _ => {
1610 return Err(FusionStateCodecError::InvalidState {
1611 reason: "invalid GNSS system tag".to_string(),
1612 });
1613 }
1614 };
1615 Ok(SerializableSatelliteId {
1616 system,
1617 prn: read_u8(bytes, cursor, limit)?,
1618 })
1619}
1620
1621fn write_f64_matrix(
1622 bytes: &mut Vec<u8>,
1623 matrix: &[Vec<F64Bits>],
1624) -> Result<(), FusionStateCodecError> {
1625 write_u32_checked(bytes, matrix.len())?;
1626 let cols = matrix.first().map_or(0, Vec::len);
1627 write_u32_checked(bytes, cols)?;
1628 for row in matrix {
1629 if row.len() != cols {
1630 return Err(FusionStateCodecError::InvalidState {
1631 reason: "ragged matrix cannot be encoded".to_string(),
1632 });
1633 }
1634 write_f64_vec_body(bytes, row);
1635 }
1636 Ok(())
1637}
1638
1639fn read_f64_matrix(
1640 bytes: &[u8],
1641 cursor: &mut usize,
1642 limit: usize,
1643) -> Result<Vec<Vec<F64Bits>>, FusionStateCodecError> {
1644 let rows = read_u32(bytes, cursor, limit)? as usize;
1645 let cols = read_u32(bytes, cursor, limit)? as usize;
1646 if rows == 0 || cols == 0 {
1647 return Err(FusionStateCodecError::InvalidState {
1648 reason: "matrix dimensions must be positive".to_string(),
1649 });
1650 }
1651 let count = rows
1652 .checked_mul(cols)
1653 .ok_or_else(|| FusionStateCodecError::InvalidState {
1654 reason: "matrix dimensions overflow usize".to_string(),
1655 })?;
1656 let needed = count
1657 .checked_mul(8)
1658 .ok_or_else(|| FusionStateCodecError::InvalidState {
1659 reason: "matrix byte length overflows usize".to_string(),
1660 })?;
1661 ensure_available(*cursor, needed, limit)?;
1662 let mut matrix = Vec::with_capacity(rows);
1663 for _ in 0..rows {
1664 let mut row = Vec::with_capacity(cols);
1665 for _ in 0..cols {
1666 row.push(read_f64(bytes, cursor, limit)?);
1667 }
1668 matrix.push(row);
1669 }
1670 Ok(matrix)
1671}
1672
1673fn write_f64_vec(bytes: &mut Vec<u8>, values: &[F64Bits]) -> Result<(), FusionStateCodecError> {
1674 write_u32_checked(bytes, values.len())?;
1675 write_f64_vec_body(bytes, values);
1676 Ok(())
1677}
1678
1679fn write_f64_vec_body(bytes: &mut Vec<u8>, values: &[F64Bits]) {
1680 for value in values {
1681 write_f64(bytes, *value);
1682 }
1683}
1684
1685fn read_f64_vec(
1686 bytes: &[u8],
1687 cursor: &mut usize,
1688 limit: usize,
1689) -> Result<Vec<F64Bits>, FusionStateCodecError> {
1690 let len = read_u32(bytes, cursor, limit)? as usize;
1691 let needed = len
1692 .checked_mul(8)
1693 .ok_or_else(|| FusionStateCodecError::InvalidState {
1694 reason: "vector byte length overflows usize".to_string(),
1695 })?;
1696 ensure_available(*cursor, needed, limit)?;
1697 let mut values = Vec::with_capacity(len);
1698 for _ in 0..len {
1699 values.push(read_f64(bytes, cursor, limit)?);
1700 }
1701 Ok(values)
1702}
1703
1704fn write_f64_array<const N: usize>(bytes: &mut Vec<u8>, values: &[F64Bits; N]) {
1705 for value in values {
1706 write_f64(bytes, *value);
1707 }
1708}
1709
1710fn read_f64_array<const N: usize>(
1711 bytes: &[u8],
1712 cursor: &mut usize,
1713 limit: usize,
1714) -> Result<[F64Bits; N], FusionStateCodecError> {
1715 let mut out = [F64Bits { bits: 0 }; N];
1716 for value in &mut out {
1717 *value = read_f64(bytes, cursor, limit)?;
1718 }
1719 Ok(out)
1720}
1721
1722fn write_f64(bytes: &mut Vec<u8>, value: F64Bits) {
1723 write_u64(bytes, value.bits);
1724}
1725
1726fn read_f64(
1727 bytes: &[u8],
1728 cursor: &mut usize,
1729 limit: usize,
1730) -> Result<F64Bits, FusionStateCodecError> {
1731 Ok(F64Bits {
1732 bits: read_u64(bytes, cursor, limit)?,
1733 })
1734}
1735
1736fn write_u16(bytes: &mut Vec<u8>, value: u16) {
1737 bytes.extend_from_slice(&value.to_le_bytes());
1738}
1739
1740fn write_u32_checked(bytes: &mut Vec<u8>, value: usize) -> Result<(), FusionStateCodecError> {
1741 let value = u32::try_from(value).map_err(|_| FusionStateCodecError::InvalidState {
1742 reason: "length exceeds u32".to_string(),
1743 })?;
1744 bytes.extend_from_slice(&value.to_le_bytes());
1745 Ok(())
1746}
1747
1748fn write_u64(bytes: &mut Vec<u8>, value: u64) {
1749 bytes.extend_from_slice(&value.to_le_bytes());
1750}
1751
1752fn write_bool(bytes: &mut Vec<u8>, value: bool) {
1753 bytes.push(u8::from(value));
1754}
1755
1756fn read_bool(
1757 bytes: &[u8],
1758 cursor: &mut usize,
1759 limit: usize,
1760) -> Result<bool, FusionStateCodecError> {
1761 match read_u8(bytes, cursor, limit)? {
1762 0 => Ok(false),
1763 1 => Ok(true),
1764 _ => Err(FusionStateCodecError::InvalidState {
1765 reason: "invalid boolean tag".to_string(),
1766 }),
1767 }
1768}
1769
1770fn read_u8(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u8, FusionStateCodecError> {
1771 ensure_available(*cursor, 1, limit)?;
1772 let value = bytes[*cursor];
1773 *cursor += 1;
1774 Ok(value)
1775}
1776
1777fn read_u16(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u16, FusionStateCodecError> {
1778 let data = read_array::<2>(bytes, *cursor, limit)?;
1779 *cursor += 2;
1780 Ok(u16::from_le_bytes(data))
1781}
1782
1783fn read_u32(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u32, FusionStateCodecError> {
1784 let data = read_array::<4>(bytes, *cursor, limit)?;
1785 *cursor += 4;
1786 Ok(u32::from_le_bytes(data))
1787}
1788
1789fn read_u64(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u64, FusionStateCodecError> {
1790 let data = read_array::<8>(bytes, *cursor, limit)?;
1791 *cursor += 8;
1792 Ok(u64::from_le_bytes(data))
1793}
1794
1795fn read_u64_at(bytes: &[u8], offset: usize) -> Result<u64, FusionStateCodecError> {
1796 Ok(u64::from_le_bytes(read_array::<8>(
1797 bytes,
1798 offset,
1799 bytes.len(),
1800 )?))
1801}
1802
1803fn read_len(
1804 bytes: &[u8],
1805 cursor: &mut usize,
1806 limit: usize,
1807 min_element_bytes: usize,
1808 field: &'static str,
1809) -> Result<usize, FusionStateCodecError> {
1810 let len = read_u32(bytes, cursor, limit)? as usize;
1811 let needed =
1812 len.checked_mul(min_element_bytes)
1813 .ok_or_else(|| FusionStateCodecError::InvalidState {
1814 reason: format!("{field} byte length overflows usize"),
1815 })?;
1816 ensure_available(*cursor, needed, limit)?;
1817 Ok(len)
1818}
1819
1820fn read_array<const N: usize>(
1821 bytes: &[u8],
1822 offset: usize,
1823 limit: usize,
1824) -> Result<[u8; N], FusionStateCodecError> {
1825 ensure_available(offset, N, limit)?;
1826 let end = offset + N;
1827 let mut out = [0u8; N];
1828 out.copy_from_slice(&bytes[offset..end]);
1829 Ok(out)
1830}
1831
1832fn ensure_available(
1833 offset: usize,
1834 needed: usize,
1835 limit: usize,
1836) -> Result<(), FusionStateCodecError> {
1837 let end = offset
1838 .checked_add(needed)
1839 .ok_or(FusionStateCodecError::Truncated {
1840 offset,
1841 needed,
1842 actual: limit.saturating_sub(offset),
1843 })?;
1844 if end <= limit {
1845 Ok(())
1846 } else {
1847 Err(FusionStateCodecError::Truncated {
1848 offset,
1849 needed,
1850 actual: limit.saturating_sub(offset),
1851 })
1852 }
1853}
1854
1855fn fnv1a64(bytes: &[u8]) -> u64 {
1856 bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
1857 (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
1858 })
1859}
1860
1861#[cfg(test)]
1862mod tests {
1863 use super::*;
1869 use crate::astro::constants::earth::WGS84_A_M;
1870 use crate::fusion::state::{ErrorStateLayout, ERROR_STATE_DIMENSION_15};
1871 use crate::fusion::TimeSyncHistoryConfig;
1872 use crate::inertial::config::RANDOM_WALK_BIAS_TAU_S;
1873 use crate::inertial::state::mat3_identity;
1874 use crate::inertial::{ImuSample, ImuSpec, NavState};
1875
1876 fn test_filter() -> InertialFilter {
1877 let nominal = NavState::new(
1878 12.5,
1879 [WGS84_A_M, -0.0, 3.25],
1880 [0.5, -0.25, 0.125],
1881 mat3_identity(),
1882 )
1883 .expect("nominal")
1884 .with_biases([0.01, -0.02, 0.03], [-0.001, 0.002, -0.003])
1885 .expect("biases");
1886 let mut diagonal = vec![1.0; ERROR_STATE_DIMENSION_15];
1887 diagonal[0] = 4.0;
1888 diagonal[1] = 9.0;
1889 let state = InsFilterState::from_diagonal(nominal, ErrorStateLayout::Fifteen, &diagonal)
1890 .expect("state");
1891 let spec = ImuSpec::datasheet(
1892 0.0,
1893 0.0,
1894 0.0,
1895 0.0,
1896 RANDOM_WALK_BIAS_TAU_S,
1897 RANDOM_WALK_BIAS_TAU_S,
1898 None,
1899 None,
1900 );
1901 InertialFilter::new(state, spec).expect("filter")
1902 }
1903
1904 fn increment(t_j2000_s: f64, dt_s: f64) -> ImuSample {
1905 ImuSample::increment(
1906 t_j2000_s,
1907 [0.015625 * dt_s, -0.0078125 * dt_s, 0.00390625 * dt_s],
1908 [
1909 0.0009765625 * dt_s,
1910 -0.00048828125 * dt_s,
1911 0.000244140625 * dt_s,
1912 ],
1913 dt_s,
1914 )
1915 }
1916
1917 fn measurement_at(t_j2000_s: f64, position_ecef_m: [f64; 3]) -> GnssFixMeasurement {
1918 GnssFixMeasurement::position(
1919 t_j2000_s,
1920 position_ecef_m,
1921 [[4.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 6.0]],
1922 8,
1923 )
1924 .expect("measurement")
1925 }
1926
1927 #[test]
1928 fn binary_and_json_round_trip_preserve_bits() {
1929 let filter = test_filter();
1930 let serial = filter.serializable_state().expect("serial state");
1931 let encoded = serial.encode_versioned().expect("encode");
1932 let decoded = SerializableFusionState::decode_versioned(&encoded).expect("decode");
1933 assert_eq!(decoded, serial);
1934 assert_snapshot_bits(
1935 &decoded.to_snapshot().expect("snapshot"),
1936 &filter.snapshot(),
1937 );
1938
1939 let json = serial.to_json_string().expect("json");
1940 let decoded_json = SerializableFusionState::from_json_str(&json).expect("json decode");
1941 assert_eq!(decoded_json, serial);
1942 assert_snapshot_bits(
1943 &decoded_json.to_snapshot().expect("json snapshot"),
1944 &filter.snapshot(),
1945 );
1946 }
1947
1948 #[test]
1949 fn truncated_and_corrupted_payloads_are_typed_errors() {
1950 let serial = test_filter().serializable_state().expect("serial state");
1951 let encoded = serial.encode_versioned().expect("encode");
1952 let truncated = &encoded[..encoded.len() - 3];
1953 assert!(matches!(
1954 SerializableFusionState::decode_versioned(truncated),
1955 Err(FusionStateCodecError::Checksum { .. })
1956 ));
1957
1958 let mut corrupted = encoded;
1959 let idx = corrupted.len() / 2;
1960 corrupted[idx] ^= 0x55;
1961 assert!(matches!(
1962 SerializableFusionState::decode_versioned(&corrupted),
1963 Err(FusionStateCodecError::Checksum { .. })
1964 ));
1965
1966 let too_short = [0u8; 5];
1967 assert!(matches!(
1968 SerializableFusionState::decode_versioned(&too_short),
1969 Err(FusionStateCodecError::Truncated { .. })
1970 ));
1971 }
1972
1973 #[test]
1974 fn malformed_matrix_dimensions_are_typed_errors() {
1975 let mut bytes = Vec::new();
1976 bytes.extend_from_slice(&u32::MAX.to_le_bytes());
1977 bytes.extend_from_slice(&0u32.to_le_bytes());
1978 let mut cursor = 0usize;
1979 assert!(matches!(
1980 read_f64_matrix(&bytes, &mut cursor, bytes.len()),
1981 Err(FusionStateCodecError::InvalidState { .. })
1982 ));
1983 }
1984
1985 #[test]
1986 fn restored_encoded_state_retains_time_sync_history_for_late_replay_bits() {
1987 let first = measurement_at(13.0, [WGS84_A_M + 0.25, -0.125, 3.5]);
1988 let late = measurement_at(13.25, [WGS84_A_M - 0.0625, 0.1875, 3.0]);
1989 let final_fix = measurement_at(13.5, [WGS84_A_M + 0.03125, -0.25, 3.125]);
1990
1991 let mut direct = test_filter();
1992 direct
1993 .configure_time_sync_history(TimeSyncHistoryConfig::new(8, 8))
1994 .expect("history");
1995 direct.propagate(increment(13.0, 0.5)).expect("imu");
1996 direct.update_loose(&first).expect("first");
1997 direct.propagate(increment(13.25, 0.25)).expect("imu");
1998 direct.update_loose(&late).expect("late in order");
1999 direct.propagate(increment(13.5, 0.25)).expect("imu");
2000 direct.update_loose(&final_fix).expect("final");
2001 direct.propagate(increment(14.0, 0.5)).expect("imu");
2002
2003 let mut delayed = test_filter();
2004 delayed
2005 .configure_time_sync_history(TimeSyncHistoryConfig::new(8, 8))
2006 .expect("history");
2007 delayed.propagate(increment(13.0, 0.5)).expect("imu");
2008 delayed.update_loose(&first).expect("first");
2009 delayed.propagate(increment(13.5, 0.5)).expect("imu");
2010 delayed.update_loose(&final_fix).expect("final");
2011 delayed.propagate(increment(14.0, 0.5)).expect("imu");
2012 let encoded = delayed.encode_state().expect("encode");
2013
2014 let mut restored = test_filter();
2015 restored.restore_encoded_state(&encoded).expect("restore");
2016 let update = restored
2017 .update_loose_time_sync(&late)
2018 .expect("late replay after restore");
2019
2020 assert!(update.late_measurement);
2021 assert_snapshot_bits(&restored.snapshot(), &direct.snapshot());
2022 }
2023
2024 fn assert_snapshot_bits(actual: &InertialFilterSnapshot, expected: &InertialFilterSnapshot) {
2025 assert_eq!(
2026 actual.state.nominal.t_j2000_s.to_bits(),
2027 expected.state.nominal.t_j2000_s.to_bits()
2028 );
2029 for axis in 0..3 {
2030 assert_eq!(
2031 actual.state.nominal.position_ecef_m[axis].to_bits(),
2032 expected.state.nominal.position_ecef_m[axis].to_bits()
2033 );
2034 assert_eq!(
2035 actual.last_body_rate_wrt_ecef_rps[axis].to_bits(),
2036 expected.last_body_rate_wrt_ecef_rps[axis].to_bits()
2037 );
2038 }
2039 for row in 0..actual.state.covariance.len() {
2040 for col in 0..actual.state.covariance[row].len() {
2041 assert_eq!(
2042 actual.state.covariance[row][col].to_bits(),
2043 expected.state.covariance[row][col].to_bits()
2044 );
2045 }
2046 }
2047 assert_eq!(
2048 actual.tight.clock_bias_m.to_bits(),
2049 expected.tight.clock_bias_m.to_bits()
2050 );
2051 assert_eq!(
2052 actual.tight.clock_drift_m_s.to_bits(),
2053 expected.tight.clock_drift_m_s.to_bits()
2054 );
2055 for row in 0..actual.tight.augmented_covariance.len() {
2056 for col in 0..actual.tight.augmented_covariance[row].len() {
2057 assert_eq!(
2058 actual.tight.augmented_covariance[row][col].to_bits(),
2059 expected.tight.augmented_covariance[row][col].to_bits()
2060 );
2061 }
2062 }
2063 }
2064}