Skip to main content

sidereon_core/fusion/
serial.rs

1//! Versioned serialization for fusion filter checkpoints.
2
3use 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
23/// Current binary and serde schema version for fusion checkpoints.
24pub const FUSION_STATE_CODEC_VERSION: u16 = 1;
25
26/// Exact JSON representation of an `f64` bit pattern.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
28#[serde(transparent)]
29pub struct F64Bits {
30    /// IEEE-754 binary64 bit pattern.
31    pub bits: u64,
32}
33
34impl F64Bits {
35    /// Convert an `f64` to its raw bit representation.
36    pub const fn from_f64(value: f64) -> Self {
37        Self {
38            bits: value.to_bits(),
39        }
40    }
41
42    /// Convert the raw bit representation back to `f64`.
43    pub const fn to_f64(self) -> f64 {
44        f64::from_bits(self.bits)
45    }
46}
47
48/// Serializable error-state layout tag.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50pub enum SerializableErrorStateLayout {
51    /// Fifteen-state layout `dr, dv, psi, b_a, b_g`.
52    Fifteen,
53    /// Twenty-one-state layout with accelerometer and gyroscope scale factors.
54    TwentyOne,
55}
56
57impl SerializableErrorStateLayout {
58    /// Convert a native layout into its stable serialized tag.
59    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    /// Convert the stable serialized tag back to the native layout.
67    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/// Serializable navigation state with exact floating-point bit storage.
76#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
77pub struct SerializableNavState {
78    /// State time in seconds since J2000, stored as raw `f64` bits.
79    pub t_j2000_s: F64Bits,
80    /// IMU ECEF position in meters, stored as raw `f64` bits.
81    pub position_ecef_m: [F64Bits; 3],
82    /// IMU ECEF velocity in meters per second, stored as raw `f64` bits.
83    pub velocity_ecef_mps: [F64Bits; 3],
84    /// Body-to-ECEF direction cosine matrix, stored as raw `f64` bits.
85    pub attitude_body_to_ecef: [[F64Bits; 3]; 3],
86    /// Closed-loop accelerometer bias estimate, stored as raw `f64` bits.
87    pub accel_bias_mps2: [F64Bits; 3],
88    /// Closed-loop gyroscope bias estimate, stored as raw `f64` bits.
89    pub gyro_bias_rps: [F64Bits; 3],
90}
91
92impl SerializableNavState {
93    /// Convert a native navigation state into the stable serialized form.
94    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    /// Convert the stable serialized form back to a validated native state.
106    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/// Serializable INS filter state and covariance.
121#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122pub struct SerializableInsFilterState {
123    /// Error-state covariance layout.
124    pub layout: SerializableErrorStateLayout,
125    /// Nonlinear mechanized navigation state.
126    pub nominal: SerializableNavState,
127    /// Error-state vector values, stored as raw `f64` bits.
128    pub error_state: Vec<F64Bits>,
129    /// Error-state covariance matrix, stored as raw `f64` bits.
130    pub covariance: Vec<Vec<F64Bits>>,
131    /// Accelerometer scale-factor estimates, stored as raw `f64` bits.
132    pub accel_scale_factor: [F64Bits; 3],
133    /// Gyroscope scale-factor estimates, stored as raw `f64` bits.
134    pub gyro_scale_factor: [F64Bits; 3],
135}
136
137impl SerializableInsFilterState {
138    /// Convert a native INS filter state into the stable serialized form.
139    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    /// Convert the stable serialized form back to a validated native state.
151    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/// Serializable tight receiver-clock augmentation.
169#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
170pub struct SerializableTightFilterState {
171    /// Receiver-clock range bias in meters, stored as raw `f64` bits.
172    pub clock_bias_m: F64Bits,
173    /// Receiver-clock drift in meters per second, stored as raw `f64` bits.
174    pub clock_drift_m_s: F64Bits,
175    /// Full augmented covariance, stored as raw `f64` bits.
176    pub augmented_covariance: Vec<Vec<F64Bits>>,
177}
178
179impl SerializableTightFilterState {
180    /// Convert a native tight snapshot into the stable serialized form.
181    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    /// Convert the stable serialized form back to a native tight snapshot.
190    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/// Serializable retained-history capacity settings.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
216pub struct SerializableTimeSyncHistoryConfig {
217    /// Number of retained IMU samples.
218    pub imu_capacity: u32,
219    /// Number of retained GNSS checkpoints.
220    pub checkpoint_capacity: u32,
221}
222
223impl SerializableTimeSyncHistoryConfig {
224    /// Convert native time-sync capacity settings into the serialized form.
225    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    /// Convert the serialized capacity settings back to native settings.
233    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/// Serializable GNSS satellite identifier.
244#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
245pub struct SerializableSatelliteId {
246    /// GNSS constellation.
247    pub system: GnssSystem,
248    /// Within-system PRN or slot number.
249    pub prn: u8,
250}
251
252impl SerializableSatelliteId {
253    /// Convert a native satellite identifier into the serialized form.
254    pub const fn from_native(id: GnssSatelliteId) -> Self {
255        Self {
256            system: id.system,
257            prn: id.prn,
258        }
259    }
260
261    /// Convert the serialized identifier back to the native type.
262    pub fn to_native(self) -> Result<GnssSatelliteId, FusionStateCodecError> {
263        GnssSatelliteId::new(self.system, self.prn).map_err(invalid_state)
264    }
265}
266
267/// Serializable body-rate interpolation endpoint for retained IMU samples.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
269pub struct SerializableRateEndpoint {
270    /// Endpoint epoch in seconds since J2000, stored as raw `f64` bits.
271    pub t_j2000_s: F64Bits,
272    /// Specific force in body axes, stored as raw `f64` bits.
273    pub specific_force_mps2: [F64Bits; 3],
274    /// Angular rate in body axes, stored as raw `f64` bits.
275    pub angular_rate_rps: [F64Bits; 3],
276}
277
278impl SerializableRateEndpoint {
279    /// Convert a native rate endpoint into the serialized form.
280    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    /// Convert the serialized endpoint back to native values.
289    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/// Serializable IMU sample payload.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
300pub enum SerializableImuSampleKind {
301    /// Specific force and angular rate payload.
302    Rate {
303        /// Specific force in body axes, stored as raw `f64` bits.
304        specific_force_mps2: [F64Bits; 3],
305        /// Angular rate in body axes, stored as raw `f64` bits.
306        angular_rate_rps: [F64Bits; 3],
307    },
308    /// Integrated delta-velocity and delta-angle payload.
309    Increment {
310        /// Body-frame delta velocity, stored as raw `f64` bits.
311        delta_velocity_mps: [F64Bits; 3],
312        /// Body-frame delta angle, stored as raw `f64` bits.
313        delta_theta_rad: [F64Bits; 3],
314        /// Sample integration interval, stored as raw `f64` bits.
315        dt_s: F64Bits,
316    },
317}
318
319impl SerializableImuSampleKind {
320    /// Convert a native IMU sample payload into the serialized form.
321    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    /// Convert the serialized payload back to native IMU sample values.
343    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/// Serializable IMU sample.
366#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
367pub struct SerializableImuSample {
368    /// Sample end epoch in seconds since J2000, stored as raw `f64` bits.
369    pub t_j2000_s: F64Bits,
370    /// Sample payload.
371    pub kind: SerializableImuSampleKind,
372}
373
374impl SerializableImuSample {
375    /// Convert a native IMU sample into the serialized form.
376    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    /// Convert the serialized sample back to native IMU sample values.
384    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/// Serializable retained IMU sample with its replay interval metadata.
393#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
394pub struct SerializableStoredImuSample {
395    /// Previous sample boundary in seconds since J2000, stored as raw `f64` bits.
396    pub previous_t_j2000_s: F64Bits,
397    /// Stored sample at the interval end.
398    pub sample: SerializableImuSample,
399    /// Previous rate endpoint for fractional rate replay.
400    pub previous_rate: Option<SerializableRateEndpoint>,
401}
402
403impl SerializableStoredImuSample {
404    /// Convert a retained IMU sample into the serialized form.
405    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    /// Convert the serialized retained IMU sample back to native values.
416    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/// Serializable loose GNSS fix measurement.
426#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
427pub struct SerializableLooseMeasurement {
428    /// Measurement epoch in seconds since J2000, stored as raw `f64` bits.
429    pub t_j2000_s: F64Bits,
430    /// GNSS antenna position in ECEF meters, stored as raw `f64` bits.
431    pub position_ecef_m: [F64Bits; 3],
432    /// Optional GNSS antenna velocity in ECEF meters per second.
433    pub velocity_ecef_mps: Option<[F64Bits; 3]>,
434    /// Measurement covariance, stored as raw `f64` bits.
435    pub covariance: Vec<Vec<F64Bits>>,
436    /// Number of satellites used by the upstream GNSS fix.
437    pub satellites_used: u32,
438    /// Whether the upstream GNSS solver reported a successful fix.
439    pub solution_valid: bool,
440}
441
442impl SerializableLooseMeasurement {
443    /// Convert a native loose measurement into the serialized form.
444    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    /// Convert the serialized loose measurement back to native values.
456    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/// Serializable tight range-rate observation.
471#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
472pub struct SerializableTightRangeRateObservation {
473    /// Measured range rate, stored as raw `f64` bits.
474    pub measured_range_rate_m_s: F64Bits,
475    /// One-sigma range-rate uncertainty, stored as raw `f64` bits.
476    pub sigma_m_s: F64Bits,
477    /// Satellite clock drift as a range-rate bias, stored as raw `f64` bits.
478    pub satellite_clock_drift_m_s: F64Bits,
479}
480
481impl SerializableTightRangeRateObservation {
482    /// Convert a native range-rate observation into the serialized form.
483    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    /// Convert the serialized range-rate observation back to native values.
492    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/// Serializable tight carrier-phase observation.
502#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
503pub struct SerializableTightCarrierPhaseObservation {
504    /// Carrier phase converted to range units, stored as raw `f64` bits.
505    pub phase_range_m: F64Bits,
506    /// One-sigma carrier-phase range uncertainty, stored as raw `f64` bits.
507    pub sigma_m: F64Bits,
508    /// Current float ambiguity estimate, stored as raw `f64` bits.
509    pub float_ambiguity_m: F64Bits,
510}
511
512impl SerializableTightCarrierPhaseObservation {
513    /// Convert a native carrier-phase observation into the serialized form.
514    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    /// Convert the serialized carrier-phase observation back to native values.
523    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/// Serializable tight raw GNSS observation.
533#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
534pub struct SerializableTightGnssObservation {
535    /// Satellite identifier.
536    pub satellite_id: SerializableSatelliteId,
537    /// Measured code pseudorange, stored as raw `f64` bits.
538    pub pseudorange_m: F64Bits,
539    /// One-sigma pseudorange uncertainty, stored as raw `f64` bits.
540    pub pseudorange_sigma_m: F64Bits,
541    /// Optional Doppler-derived range-rate row.
542    pub range_rate: Option<SerializableTightRangeRateObservation>,
543    /// Optional carrier-phase row.
544    pub carrier_phase: Option<SerializableTightCarrierPhaseObservation>,
545    /// Ionospheric group delay correction for code, stored as raw `f64` bits.
546    pub ionosphere_delay_m: F64Bits,
547    /// Tropospheric delay correction, stored as raw `f64` bits.
548    pub troposphere_delay_m: F64Bits,
549}
550
551impl SerializableTightGnssObservation {
552    /// Convert a native tight observation into the serialized form.
553    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    /// Convert the serialized tight observation back to native values.
570    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/// Serializable tight raw GNSS epoch.
590#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
591pub struct SerializableTightGnssEpoch {
592    /// Measurement epoch in seconds since J2000, stored as raw `f64` bits.
593    pub t_j2000_s: F64Bits,
594    /// Satellite observations at the epoch.
595    pub observations: Vec<SerializableTightGnssObservation>,
596}
597
598impl SerializableTightGnssEpoch {
599    /// Convert a native tight epoch into the serialized form.
600    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    /// Convert the serialized tight epoch back to native values.
613    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/// Serializable retained GNSS measurement.
630#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
631pub enum SerializableStoredGnssMeasurement {
632    /// Retained loose GNSS fix.
633    Loose(SerializableLooseMeasurement),
634    /// Retained tight raw GNSS epoch.
635    Tight(SerializableTightGnssEpoch),
636}
637
638impl SerializableStoredGnssMeasurement {
639    /// Convert a retained GNSS measurement into the serialized form.
640    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    /// Convert the serialized GNSS measurement back to native values.
652    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/// Serializable retained filter checkpoint.
661#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
662pub struct SerializableStoredCheckpoint {
663    /// Checkpoint epoch in seconds since J2000, stored as raw `f64` bits.
664    pub t_j2000_s: F64Bits,
665    /// Closed-loop filter snapshot at the checkpoint.
666    pub snapshot: Box<SerializableFusionSnapshot>,
667}
668
669impl SerializableStoredCheckpoint {
670    /// Convert a retained checkpoint into the serialized form.
671    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    /// Convert the serialized checkpoint back to native values.
681    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/// Stable serializable fusion snapshot without retained replay history.
697#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
698pub struct SerializableFusionSnapshot {
699    /// Closed-loop INS state and covariance.
700    pub state: SerializableInsFilterState,
701    /// Last propagated body angular rate relative to ECEF, stored as raw `f64` bits.
702    pub last_body_rate_wrt_ecef_rps: [F64Bits; 3],
703    /// Tight receiver-clock augmentation and augmented covariance.
704    pub tight: SerializableTightFilterState,
705}
706
707impl SerializableFusionSnapshot {
708    /// Convert a native fusion snapshot into the stable serialized form.
709    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    /// Convert the stable serialized form back to a validated native snapshot.
718    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/// Serializable retained time-sync replay history.
733#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
734pub struct SerializableTimeSyncHistory {
735    /// Retained-history capacity settings.
736    pub config: SerializableTimeSyncHistoryConfig,
737    /// Retained IMU samples for replay.
738    pub imu_samples: Vec<SerializableStoredImuSample>,
739    /// Retained filter checkpoints at GNSS epochs.
740    pub checkpoints: Vec<SerializableStoredCheckpoint>,
741    /// Retained GNSS measurements for replay ordering.
742    pub measurements: Vec<SerializableStoredGnssMeasurement>,
743}
744
745impl SerializableTimeSyncHistory {
746    /// Convert native retained history into the serialized form.
747    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    /// Convert the serialized retained history back to native values.
770    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/// Stable serializable fusion checkpoint.
795#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
796pub struct SerializableFusionState {
797    /// Serialization schema version.
798    pub version: u16,
799    /// Closed-loop INS state and covariance.
800    pub state: SerializableInsFilterState,
801    /// Last propagated body angular rate relative to ECEF, stored as raw `f64` bits.
802    pub last_body_rate_wrt_ecef_rps: [F64Bits; 3],
803    /// Tight receiver-clock augmentation and augmented covariance.
804    pub tight: SerializableTightFilterState,
805    /// Retained time-sync replay history.
806    pub time_sync: SerializableTimeSyncHistory,
807}
808
809impl SerializableFusionState {
810    /// Convert a native fusion snapshot into the stable serialized form.
811    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    /// Convert a live fusion filter into the stable serialized form.
825    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    /// Convert the stable serialized form back to a validated native snapshot.
839    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    /// Convert the stable serialized form back to retained replay history.
854    fn to_time_sync_history(&self) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
855        self.validate_version()?;
856        self.time_sync.to_native()
857    }
858
859    /// Encode the checkpoint with a binary magic, version, and checksum.
860    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    /// Decode a binary checkpoint produced by [`Self::encode_versioned`].
882    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    /// Serialize this checkpoint to JSON using the serde representation.
946    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    /// Parse a JSON checkpoint using the serde representation.
954    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    /// Convert this snapshot into a stable serializable checkpoint.
977    pub fn to_serializable_fusion_state(&self) -> SerializableFusionState {
978        SerializableFusionState::from_snapshot(self)
979    }
980
981    /// Encode this snapshot with the versioned binary fusion codec.
982    pub fn encode_fusion_state(&self) -> Result<Vec<u8>, FusionStateCodecError> {
983        self.to_serializable_fusion_state().encode_versioned()
984    }
985
986    /// Decode a snapshot from the versioned binary fusion codec.
987    pub fn decode_fusion_state(bytes: &[u8]) -> Result<Self, FusionStateCodecError> {
988        SerializableFusionState::decode_versioned(bytes)?.to_snapshot()
989    }
990}
991
992impl InertialFilter {
993    /// Return the current filter checkpoint in the stable serializable form.
994    pub fn serializable_state(&self) -> Result<SerializableFusionState, FusionStateCodecError> {
995        SerializableFusionState::from_filter(self)
996    }
997
998    /// Encode the current filter checkpoint with the versioned binary codec.
999    pub fn encode_state(&self) -> Result<Vec<u8>, FusionStateCodecError> {
1000        SerializableFusionState::from_filter(self)?.encode_versioned()
1001    }
1002
1003    /// Restore this filter from a stable serializable checkpoint.
1004    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    /// Restore this filter from the versioned binary fusion codec.
1017    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/// Errors returned by the versioned fusion-state codec.
1024#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1025pub enum FusionStateCodecError {
1026    /// The binary payload did not begin with the fusion-state magic bytes.
1027    #[error("fusion state payload has invalid magic")]
1028    InvalidMagic,
1029    /// The payload schema version is not supported by this decoder.
1030    #[error("fusion state version {version} is not supported")]
1031    UnsupportedVersion {
1032        /// Version tag found in the payload.
1033        version: u16,
1034    },
1035    /// The payload ended before a complete field could be read.
1036    #[error("fusion state payload truncated at {offset}, needed {needed} bytes, got {actual}")]
1037    Truncated {
1038        /// Byte offset of the attempted read.
1039        offset: usize,
1040        /// Number of bytes required for the read or minimum payload.
1041        needed: usize,
1042        /// Number of bytes available in the payload or read window.
1043        actual: usize,
1044    },
1045    /// The checksum did not match the decoded payload bytes.
1046    #[error("fusion state checksum expected {expected:#x} but found {found:#x}")]
1047    Checksum {
1048        /// Checksum stored in the payload.
1049        expected: u64,
1050        /// Checksum computed from the payload.
1051        found: u64,
1052    },
1053    /// The payload contained bytes after the versioned fields.
1054    #[error("fusion state payload has {remaining} trailing bytes")]
1055    TrailingBytes {
1056        /// Number of bytes not consumed before the checksum.
1057        remaining: usize,
1058    },
1059    /// The payload decoded but did not validate as a native fusion state.
1060    #[error("invalid fusion state payload: {reason}")]
1061    InvalidState {
1062        /// Stable validation reason.
1063        reason: String,
1064    },
1065    /// JSON serde encoding or decoding failed.
1066    #[error("fusion state JSON error: {message}")]
1067    Json {
1068        /// Serde error text.
1069        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    //! Provenance: serial codec tests assert the documented ABI contract for
1864    //! versioned little-endian fields and FNV-1a corruption detection. The
1865    //! numeric oracle is exact IEEE-754 bit identity before and after binary and
1866    //! serde JSON round trips.
1867
1868    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}