use super::loose::{GnssFixMeasurement, InertialFilter};
use super::state::{
validate_covariance_matrix, validate_finite_slice, ErrorStateLayout, ErrorStateVector,
InsFilterState,
};
use super::tight::{
augmented_dimension, TightCarrierPhaseObservation, TightFilterSnapshot, TightGnssEpoch,
TightGnssObservation, TightRangeRateObservation,
};
use super::timesync::{
InertialFilterSnapshot, RateEndpoint, StoredCheckpoint, StoredGnssMeasurement, StoredImuSample,
TimeSyncHistoryConfig, TimeSyncHistorySnapshot,
};
use crate::inertial::{ImuSample, ImuSampleKind, NavState};
use crate::{GnssSatelliteId, GnssSystem};
const FUSION_STATE_MAGIC: [u8; 8] = *b"FUSSTAT\0";
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
pub const FUSION_STATE_CODEC_VERSION: u16 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct F64Bits {
pub bits: u64,
}
impl F64Bits {
pub const fn from_f64(value: f64) -> Self {
Self {
bits: value.to_bits(),
}
}
pub const fn to_f64(self) -> f64 {
f64::from_bits(self.bits)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerializableErrorStateLayout {
Fifteen,
TwentyOne,
}
impl SerializableErrorStateLayout {
pub const fn from_native(layout: ErrorStateLayout) -> Self {
match layout {
ErrorStateLayout::Fifteen => Self::Fifteen,
ErrorStateLayout::TwentyOne => Self::TwentyOne,
}
}
pub const fn to_native(self) -> ErrorStateLayout {
match self {
Self::Fifteen => ErrorStateLayout::Fifteen,
Self::TwentyOne => ErrorStateLayout::TwentyOne,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableNavState {
pub t_j2000_s: F64Bits,
pub position_ecef_m: [F64Bits; 3],
pub velocity_ecef_mps: [F64Bits; 3],
pub attitude_body_to_ecef: [[F64Bits; 3]; 3],
pub accel_bias_mps2: [F64Bits; 3],
pub gyro_bias_rps: [F64Bits; 3],
}
impl SerializableNavState {
pub fn from_native(state: &NavState) -> Self {
Self {
t_j2000_s: F64Bits::from_f64(state.t_j2000_s),
position_ecef_m: bits3(state.position_ecef_m),
velocity_ecef_mps: bits3(state.velocity_ecef_mps),
attitude_body_to_ecef: bits3x3(state.attitude_body_to_ecef),
accel_bias_mps2: bits3(state.accel_bias_mps2),
gyro_bias_rps: bits3(state.gyro_bias_rps),
}
}
pub fn to_native(&self) -> Result<NavState, FusionStateCodecError> {
let state = NavState {
t_j2000_s: self.t_j2000_s.to_f64(),
position_ecef_m: f643(self.position_ecef_m),
velocity_ecef_mps: f643(self.velocity_ecef_mps),
attitude_body_to_ecef: f643x3(self.attitude_body_to_ecef),
accel_bias_mps2: f643(self.accel_bias_mps2),
gyro_bias_rps: f643(self.gyro_bias_rps),
};
state.validate().map_err(invalid_state)?;
Ok(state)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableInsFilterState {
pub layout: SerializableErrorStateLayout,
pub nominal: SerializableNavState,
pub error_state: Vec<F64Bits>,
pub covariance: Vec<Vec<F64Bits>>,
pub accel_scale_factor: [F64Bits; 3],
pub gyro_scale_factor: [F64Bits; 3],
}
impl SerializableInsFilterState {
pub fn from_native(state: &InsFilterState) -> Self {
Self {
layout: SerializableErrorStateLayout::from_native(state.layout()),
nominal: SerializableNavState::from_native(&state.nominal),
error_state: bits_slice(state.error_state.as_slice()),
covariance: bits_matrix(&state.covariance),
accel_scale_factor: bits3(state.accel_scale_factor),
gyro_scale_factor: bits3(state.gyro_scale_factor),
}
}
pub fn to_native(&self) -> Result<InsFilterState, FusionStateCodecError> {
let layout = self.layout.to_native();
let nominal = self.nominal.to_native()?;
let error_state = ErrorStateVector::from_vec(layout, f64_vec(&self.error_state))
.map_err(invalid_state)?;
let state = InsFilterState {
nominal,
error_state,
covariance: f64_matrix(&self.covariance),
accel_scale_factor: f643(self.accel_scale_factor),
gyro_scale_factor: f643(self.gyro_scale_factor),
};
state.validate().map_err(invalid_state)?;
Ok(state)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTightFilterState {
pub clock_bias_m: F64Bits,
pub clock_drift_m_s: F64Bits,
pub augmented_covariance: Vec<Vec<F64Bits>>,
}
impl SerializableTightFilterState {
pub fn from_native(snapshot: &TightFilterSnapshot) -> Self {
Self {
clock_bias_m: F64Bits::from_f64(snapshot.clock_bias_m),
clock_drift_m_s: F64Bits::from_f64(snapshot.clock_drift_m_s),
augmented_covariance: bits_matrix(&snapshot.augmented_covariance),
}
}
pub fn to_native(
&self,
base_dimension: usize,
) -> Result<TightFilterSnapshot, FusionStateCodecError> {
let snapshot = TightFilterSnapshot {
clock_bias_m: self.clock_bias_m.to_f64(),
clock_drift_m_s: self.clock_drift_m_s.to_f64(),
augmented_covariance: f64_matrix(&self.augmented_covariance),
};
validate_finite_slice(
&[snapshot.clock_bias_m, snapshot.clock_drift_m_s],
"tight_clock",
)
.map_err(invalid_state)?;
validate_covariance_matrix(
&snapshot.augmented_covariance,
augmented_dimension(base_dimension),
"tight_augmented_covariance",
)
.map_err(invalid_state)?;
Ok(snapshot)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTimeSyncHistoryConfig {
pub imu_capacity: u32,
pub checkpoint_capacity: u32,
}
impl SerializableTimeSyncHistoryConfig {
pub fn from_native(config: TimeSyncHistoryConfig) -> Result<Self, FusionStateCodecError> {
Ok(Self {
imu_capacity: checked_u32(config.imu_capacity)?,
checkpoint_capacity: checked_u32(config.checkpoint_capacity)?,
})
}
pub fn to_native(self) -> Result<TimeSyncHistoryConfig, FusionStateCodecError> {
let config = TimeSyncHistoryConfig::new(
self.imu_capacity as usize,
self.checkpoint_capacity as usize,
);
config.validate().map_err(invalid_state)?;
Ok(config)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableSatelliteId {
pub system: GnssSystem,
pub prn: u8,
}
impl SerializableSatelliteId {
pub const fn from_native(id: GnssSatelliteId) -> Self {
Self {
system: id.system,
prn: id.prn,
}
}
pub fn to_native(self) -> Result<GnssSatelliteId, FusionStateCodecError> {
GnssSatelliteId::new(self.system, self.prn).map_err(invalid_state)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableRateEndpoint {
pub t_j2000_s: F64Bits,
pub specific_force_mps2: [F64Bits; 3],
pub angular_rate_rps: [F64Bits; 3],
}
impl SerializableRateEndpoint {
fn from_native(endpoint: RateEndpoint) -> Self {
Self {
t_j2000_s: F64Bits::from_f64(endpoint.t_j2000_s),
specific_force_mps2: bits3(endpoint.specific_force_mps2),
angular_rate_rps: bits3(endpoint.angular_rate_rps),
}
}
fn to_native(self) -> RateEndpoint {
RateEndpoint {
t_j2000_s: self.t_j2000_s.to_f64(),
specific_force_mps2: f643(self.specific_force_mps2),
angular_rate_rps: f643(self.angular_rate_rps),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerializableImuSampleKind {
Rate {
specific_force_mps2: [F64Bits; 3],
angular_rate_rps: [F64Bits; 3],
},
Increment {
delta_velocity_mps: [F64Bits; 3],
delta_theta_rad: [F64Bits; 3],
dt_s: F64Bits,
},
}
impl SerializableImuSampleKind {
pub fn from_native(kind: ImuSampleKind) -> Self {
match kind {
ImuSampleKind::Rate {
specific_force_mps2,
angular_rate_rps,
} => Self::Rate {
specific_force_mps2: bits3(specific_force_mps2),
angular_rate_rps: bits3(angular_rate_rps),
},
ImuSampleKind::Increment {
delta_velocity_mps,
delta_theta_rad,
dt_s,
} => Self::Increment {
delta_velocity_mps: bits3(delta_velocity_mps),
delta_theta_rad: bits3(delta_theta_rad),
dt_s: F64Bits::from_f64(dt_s),
},
}
}
pub fn to_native(self) -> ImuSampleKind {
match self {
Self::Rate {
specific_force_mps2,
angular_rate_rps,
} => ImuSampleKind::Rate {
specific_force_mps2: f643(specific_force_mps2),
angular_rate_rps: f643(angular_rate_rps),
},
Self::Increment {
delta_velocity_mps,
delta_theta_rad,
dt_s,
} => ImuSampleKind::Increment {
delta_velocity_mps: f643(delta_velocity_mps),
delta_theta_rad: f643(delta_theta_rad),
dt_s: dt_s.to_f64(),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableImuSample {
pub t_j2000_s: F64Bits,
pub kind: SerializableImuSampleKind,
}
impl SerializableImuSample {
pub fn from_native(sample: ImuSample) -> Self {
Self {
t_j2000_s: F64Bits::from_f64(sample.t_j2000_s),
kind: SerializableImuSampleKind::from_native(sample.kind),
}
}
pub fn to_native(self) -> ImuSample {
ImuSample {
t_j2000_s: self.t_j2000_s.to_f64(),
kind: self.kind.to_native(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableStoredImuSample {
pub previous_t_j2000_s: F64Bits,
pub sample: SerializableImuSample,
pub previous_rate: Option<SerializableRateEndpoint>,
}
impl SerializableStoredImuSample {
fn from_native(sample: StoredImuSample) -> Self {
Self {
previous_t_j2000_s: F64Bits::from_f64(sample.previous_t_j2000_s),
sample: SerializableImuSample::from_native(sample.sample),
previous_rate: sample
.previous_rate
.map(SerializableRateEndpoint::from_native),
}
}
fn to_native(self) -> StoredImuSample {
StoredImuSample {
previous_t_j2000_s: self.previous_t_j2000_s.to_f64(),
sample: self.sample.to_native(),
previous_rate: self.previous_rate.map(SerializableRateEndpoint::to_native),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableLooseMeasurement {
pub t_j2000_s: F64Bits,
pub position_ecef_m: [F64Bits; 3],
pub velocity_ecef_mps: Option<[F64Bits; 3]>,
pub covariance: Vec<Vec<F64Bits>>,
pub satellites_used: u32,
pub solution_valid: bool,
}
impl SerializableLooseMeasurement {
pub fn from_native(measurement: &GnssFixMeasurement) -> Result<Self, FusionStateCodecError> {
Ok(Self {
t_j2000_s: F64Bits::from_f64(measurement.t_j2000_s),
position_ecef_m: bits3(measurement.position_ecef_m),
velocity_ecef_mps: measurement.velocity_ecef_mps.map(bits3),
covariance: bits_matrix(&measurement.covariance),
satellites_used: checked_u32(measurement.satellites_used)?,
solution_valid: measurement.solution_valid,
})
}
pub fn to_native(&self) -> Result<GnssFixMeasurement, FusionStateCodecError> {
let measurement = GnssFixMeasurement {
t_j2000_s: self.t_j2000_s.to_f64(),
position_ecef_m: f643(self.position_ecef_m),
velocity_ecef_mps: self.velocity_ecef_mps.map(f643),
covariance: f64_matrix(&self.covariance),
satellites_used: self.satellites_used as usize,
solution_valid: self.solution_valid,
};
measurement.validate().map_err(invalid_state)?;
Ok(measurement)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTightRangeRateObservation {
pub measured_range_rate_m_s: F64Bits,
pub sigma_m_s: F64Bits,
pub satellite_clock_drift_m_s: F64Bits,
}
impl SerializableTightRangeRateObservation {
pub fn from_native(observation: TightRangeRateObservation) -> Self {
Self {
measured_range_rate_m_s: F64Bits::from_f64(observation.measured_range_rate_m_s),
sigma_m_s: F64Bits::from_f64(observation.sigma_m_s),
satellite_clock_drift_m_s: F64Bits::from_f64(observation.satellite_clock_drift_m_s),
}
}
pub fn to_native(self) -> TightRangeRateObservation {
TightRangeRateObservation {
measured_range_rate_m_s: self.measured_range_rate_m_s.to_f64(),
sigma_m_s: self.sigma_m_s.to_f64(),
satellite_clock_drift_m_s: self.satellite_clock_drift_m_s.to_f64(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTightCarrierPhaseObservation {
pub phase_range_m: F64Bits,
pub sigma_m: F64Bits,
pub float_ambiguity_m: F64Bits,
}
impl SerializableTightCarrierPhaseObservation {
pub fn from_native(observation: TightCarrierPhaseObservation) -> Self {
Self {
phase_range_m: F64Bits::from_f64(observation.phase_range_m),
sigma_m: F64Bits::from_f64(observation.sigma_m),
float_ambiguity_m: F64Bits::from_f64(observation.float_ambiguity_m),
}
}
pub fn to_native(self) -> TightCarrierPhaseObservation {
TightCarrierPhaseObservation {
phase_range_m: self.phase_range_m.to_f64(),
sigma_m: self.sigma_m.to_f64(),
float_ambiguity_m: self.float_ambiguity_m.to_f64(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTightGnssObservation {
pub satellite_id: SerializableSatelliteId,
pub pseudorange_m: F64Bits,
pub pseudorange_sigma_m: F64Bits,
pub range_rate: Option<SerializableTightRangeRateObservation>,
pub carrier_phase: Option<SerializableTightCarrierPhaseObservation>,
pub ionosphere_delay_m: F64Bits,
pub troposphere_delay_m: F64Bits,
}
impl SerializableTightGnssObservation {
pub fn from_native(observation: TightGnssObservation) -> Self {
Self {
satellite_id: SerializableSatelliteId::from_native(observation.satellite_id),
pseudorange_m: F64Bits::from_f64(observation.pseudorange_m),
pseudorange_sigma_m: F64Bits::from_f64(observation.pseudorange_sigma_m),
range_rate: observation
.range_rate
.map(SerializableTightRangeRateObservation::from_native),
carrier_phase: observation
.carrier_phase
.map(SerializableTightCarrierPhaseObservation::from_native),
ionosphere_delay_m: F64Bits::from_f64(observation.ionosphere_delay_m),
troposphere_delay_m: F64Bits::from_f64(observation.troposphere_delay_m),
}
}
pub fn to_native(self) -> Result<TightGnssObservation, FusionStateCodecError> {
let observation = TightGnssObservation {
satellite_id: self.satellite_id.to_native()?,
pseudorange_m: self.pseudorange_m.to_f64(),
pseudorange_sigma_m: self.pseudorange_sigma_m.to_f64(),
range_rate: self
.range_rate
.map(SerializableTightRangeRateObservation::to_native),
carrier_phase: self
.carrier_phase
.map(SerializableTightCarrierPhaseObservation::to_native),
ionosphere_delay_m: self.ionosphere_delay_m.to_f64(),
troposphere_delay_m: self.troposphere_delay_m.to_f64(),
};
observation.validate().map_err(invalid_state)?;
Ok(observation)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTightGnssEpoch {
pub t_j2000_s: F64Bits,
pub observations: Vec<SerializableTightGnssObservation>,
}
impl SerializableTightGnssEpoch {
pub fn from_native(epoch: &TightGnssEpoch) -> Self {
Self {
t_j2000_s: F64Bits::from_f64(epoch.t_j2000_s),
observations: epoch
.observations
.iter()
.copied()
.map(SerializableTightGnssObservation::from_native)
.collect(),
}
}
pub fn to_native(&self) -> Result<TightGnssEpoch, FusionStateCodecError> {
let observations = self
.observations
.iter()
.copied()
.map(SerializableTightGnssObservation::to_native)
.collect::<Result<Vec<_>, _>>()?;
let epoch = TightGnssEpoch {
t_j2000_s: self.t_j2000_s.to_f64(),
observations,
};
epoch.validate().map_err(invalid_state)?;
Ok(epoch)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SerializableStoredGnssMeasurement {
Loose(SerializableLooseMeasurement),
Tight(SerializableTightGnssEpoch),
}
impl SerializableStoredGnssMeasurement {
fn from_native(measurement: &StoredGnssMeasurement) -> Result<Self, FusionStateCodecError> {
match measurement {
StoredGnssMeasurement::Loose(measurement) => Ok(Self::Loose(
SerializableLooseMeasurement::from_native(measurement)?,
)),
StoredGnssMeasurement::Tight(epoch) => {
Ok(Self::Tight(SerializableTightGnssEpoch::from_native(epoch)))
}
}
}
fn to_native(&self) -> Result<StoredGnssMeasurement, FusionStateCodecError> {
match self {
Self::Loose(measurement) => Ok(StoredGnssMeasurement::Loose(measurement.to_native()?)),
Self::Tight(epoch) => Ok(StoredGnssMeasurement::Tight(epoch.to_native()?)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableStoredCheckpoint {
pub t_j2000_s: F64Bits,
pub snapshot: Box<SerializableFusionSnapshot>,
}
impl SerializableStoredCheckpoint {
fn from_native(checkpoint: &StoredCheckpoint) -> Self {
Self {
t_j2000_s: F64Bits::from_f64(checkpoint.t_j2000_s),
snapshot: Box::new(SerializableFusionSnapshot::from_snapshot(
&checkpoint.snapshot,
)),
}
}
fn to_native(&self) -> Result<StoredCheckpoint, FusionStateCodecError> {
let snapshot = self.snapshot.to_snapshot()?;
let checkpoint = StoredCheckpoint {
t_j2000_s: self.t_j2000_s.to_f64(),
snapshot,
};
if checkpoint.t_j2000_s != checkpoint.snapshot.state.nominal.t_j2000_s {
return Err(FusionStateCodecError::InvalidState {
reason: "checkpoint epoch must match snapshot".to_string(),
});
}
Ok(checkpoint)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableFusionSnapshot {
pub state: SerializableInsFilterState,
pub last_body_rate_wrt_ecef_rps: [F64Bits; 3],
pub tight: SerializableTightFilterState,
}
impl SerializableFusionSnapshot {
pub fn from_snapshot(snapshot: &InertialFilterSnapshot) -> Self {
Self {
state: SerializableInsFilterState::from_native(&snapshot.state),
last_body_rate_wrt_ecef_rps: bits3(snapshot.last_body_rate_wrt_ecef_rps),
tight: SerializableTightFilterState::from_native(&snapshot.tight),
}
}
pub fn to_snapshot(&self) -> Result<InertialFilterSnapshot, FusionStateCodecError> {
let state = self.state.to_native()?;
let last_body_rate_wrt_ecef_rps = f643(self.last_body_rate_wrt_ecef_rps);
validate_finite_slice(&last_body_rate_wrt_ecef_rps, "last_body_rate_wrt_ecef_rps")
.map_err(invalid_state)?;
let tight = self.tight.to_native(state.dimension())?;
Ok(InertialFilterSnapshot {
state,
last_body_rate_wrt_ecef_rps,
tight,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableTimeSyncHistory {
pub config: SerializableTimeSyncHistoryConfig,
pub imu_samples: Vec<SerializableStoredImuSample>,
pub checkpoints: Vec<SerializableStoredCheckpoint>,
pub measurements: Vec<SerializableStoredGnssMeasurement>,
}
impl SerializableTimeSyncHistory {
fn from_native(history: &TimeSyncHistorySnapshot) -> Result<Self, FusionStateCodecError> {
Ok(Self {
config: SerializableTimeSyncHistoryConfig::from_native(history.config)?,
imu_samples: history
.imu_samples
.iter()
.copied()
.map(SerializableStoredImuSample::from_native)
.collect(),
checkpoints: history
.checkpoints
.iter()
.map(SerializableStoredCheckpoint::from_native)
.collect(),
measurements: history
.measurements
.iter()
.map(SerializableStoredGnssMeasurement::from_native)
.collect::<Result<Vec<_>, _>>()?,
})
}
fn to_native(&self) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
let snapshot = TimeSyncHistorySnapshot {
config: self.config.to_native()?,
imu_samples: self
.imu_samples
.iter()
.copied()
.map(SerializableStoredImuSample::to_native)
.collect(),
checkpoints: self
.checkpoints
.iter()
.map(SerializableStoredCheckpoint::to_native)
.collect::<Result<Vec<_>, _>>()?,
measurements: self
.measurements
.iter()
.map(SerializableStoredGnssMeasurement::to_native)
.collect::<Result<Vec<_>, _>>()?,
};
validate_history_by_restore(snapshot)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SerializableFusionState {
pub version: u16,
pub state: SerializableInsFilterState,
pub last_body_rate_wrt_ecef_rps: [F64Bits; 3],
pub tight: SerializableTightFilterState,
pub time_sync: SerializableTimeSyncHistory,
}
impl SerializableFusionState {
pub fn from_snapshot(snapshot: &InertialFilterSnapshot) -> Self {
let history = TimeSyncHistorySnapshot::from_filter_snapshot(snapshot.clone());
let time_sync =
SerializableTimeSyncHistory::from_native(&history).expect("default history encodes");
Self {
version: FUSION_STATE_CODEC_VERSION,
state: SerializableInsFilterState::from_native(&snapshot.state),
last_body_rate_wrt_ecef_rps: bits3(snapshot.last_body_rate_wrt_ecef_rps),
tight: SerializableTightFilterState::from_native(&snapshot.tight),
time_sync,
}
}
pub fn from_filter(filter: &InertialFilter) -> Result<Self, FusionStateCodecError> {
let snapshot = filter.snapshot();
Ok(Self {
version: FUSION_STATE_CODEC_VERSION,
state: SerializableInsFilterState::from_native(&snapshot.state),
last_body_rate_wrt_ecef_rps: bits3(snapshot.last_body_rate_wrt_ecef_rps),
tight: SerializableTightFilterState::from_native(&snapshot.tight),
time_sync: SerializableTimeSyncHistory::from_native(
&filter.time_sync.snapshot_history(),
)?,
})
}
pub fn to_snapshot(&self) -> Result<InertialFilterSnapshot, FusionStateCodecError> {
self.validate_version()?;
let state = self.state.to_native()?;
let last_body_rate_wrt_ecef_rps = f643(self.last_body_rate_wrt_ecef_rps);
validate_finite_slice(&last_body_rate_wrt_ecef_rps, "last_body_rate_wrt_ecef_rps")
.map_err(invalid_state)?;
let tight = self.tight.to_native(state.dimension())?;
Ok(InertialFilterSnapshot {
state,
last_body_rate_wrt_ecef_rps,
tight,
})
}
fn to_time_sync_history(&self) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
self.validate_version()?;
self.time_sync.to_native()
}
pub fn encode_versioned(&self) -> Result<Vec<u8>, FusionStateCodecError> {
self.validate_version()?;
let mut bytes = Vec::new();
bytes.extend_from_slice(&FUSION_STATE_MAGIC);
write_u16(&mut bytes, self.version);
write_layout(&mut bytes, self.state.layout);
write_nav(&mut bytes, &self.state.nominal);
write_f64_vec(&mut bytes, &self.state.error_state)?;
write_f64_matrix(&mut bytes, &self.state.covariance)?;
write_f64_array(&mut bytes, &self.state.accel_scale_factor);
write_f64_array(&mut bytes, &self.state.gyro_scale_factor);
write_f64_array(&mut bytes, &self.last_body_rate_wrt_ecef_rps);
write_f64(&mut bytes, self.tight.clock_bias_m);
write_f64(&mut bytes, self.tight.clock_drift_m_s);
write_f64_matrix(&mut bytes, &self.tight.augmented_covariance)?;
write_time_sync_history(&mut bytes, &self.time_sync)?;
let checksum = fnv1a64(&bytes);
write_u64(&mut bytes, checksum);
Ok(bytes)
}
pub fn decode_versioned(bytes: &[u8]) -> Result<Self, FusionStateCodecError> {
let minimum = FUSION_STATE_MAGIC.len() + 2 + 8;
if bytes.len() < minimum {
return Err(FusionStateCodecError::Truncated {
offset: 0,
needed: minimum,
actual: bytes.len(),
});
}
if bytes[..FUSION_STATE_MAGIC.len()] != FUSION_STATE_MAGIC {
return Err(FusionStateCodecError::InvalidMagic);
}
let checksum_offset = bytes.len() - 8;
let expected = read_u64_at(bytes, checksum_offset)?;
let found = fnv1a64(&bytes[..checksum_offset]);
if expected != found {
return Err(FusionStateCodecError::Checksum { expected, found });
}
let mut cursor = FUSION_STATE_MAGIC.len();
let version = read_u16(bytes, &mut cursor, checksum_offset)?;
if version != FUSION_STATE_CODEC_VERSION {
return Err(FusionStateCodecError::UnsupportedVersion { version });
}
let layout = read_layout(bytes, &mut cursor, checksum_offset)?;
let nominal = read_nav(bytes, &mut cursor, checksum_offset)?;
let error_state = read_f64_vec(bytes, &mut cursor, checksum_offset)?;
let covariance = read_f64_matrix(bytes, &mut cursor, checksum_offset)?;
let accel_scale_factor = read_f64_array(bytes, &mut cursor, checksum_offset)?;
let gyro_scale_factor = read_f64_array(bytes, &mut cursor, checksum_offset)?;
let last_body_rate_wrt_ecef_rps = read_f64_array(bytes, &mut cursor, checksum_offset)?;
let clock_bias_m = read_f64(bytes, &mut cursor, checksum_offset)?;
let clock_drift_m_s = read_f64(bytes, &mut cursor, checksum_offset)?;
let augmented_covariance = read_f64_matrix(bytes, &mut cursor, checksum_offset)?;
let time_sync = read_time_sync_history(bytes, &mut cursor, checksum_offset)?;
if cursor != checksum_offset {
return Err(FusionStateCodecError::TrailingBytes {
remaining: checksum_offset - cursor,
});
}
let state = Self {
version,
state: SerializableInsFilterState {
layout,
nominal,
error_state,
covariance,
accel_scale_factor,
gyro_scale_factor,
},
last_body_rate_wrt_ecef_rps,
tight: SerializableTightFilterState {
clock_bias_m,
clock_drift_m_s,
augmented_covariance,
},
time_sync,
};
state.to_snapshot()?;
state.to_time_sync_history()?;
Ok(state)
}
pub fn to_json_string(&self) -> Result<String, FusionStateCodecError> {
self.validate_version()?;
serde_json::to_string(self).map_err(|error| FusionStateCodecError::Json {
message: error.to_string(),
})
}
pub fn from_json_str(text: &str) -> Result<Self, FusionStateCodecError> {
let state: Self =
serde_json::from_str(text).map_err(|error| FusionStateCodecError::Json {
message: error.to_string(),
})?;
state.to_snapshot()?;
state.to_time_sync_history()?;
Ok(state)
}
fn validate_version(&self) -> Result<(), FusionStateCodecError> {
if self.version == FUSION_STATE_CODEC_VERSION {
Ok(())
} else {
Err(FusionStateCodecError::UnsupportedVersion {
version: self.version,
})
}
}
}
impl InertialFilterSnapshot {
pub fn to_serializable_fusion_state(&self) -> SerializableFusionState {
SerializableFusionState::from_snapshot(self)
}
pub fn encode_fusion_state(&self) -> Result<Vec<u8>, FusionStateCodecError> {
self.to_serializable_fusion_state().encode_versioned()
}
pub fn decode_fusion_state(bytes: &[u8]) -> Result<Self, FusionStateCodecError> {
SerializableFusionState::decode_versioned(bytes)?.to_snapshot()
}
}
impl InertialFilter {
pub fn serializable_state(&self) -> Result<SerializableFusionState, FusionStateCodecError> {
SerializableFusionState::from_filter(self)
}
pub fn encode_state(&self) -> Result<Vec<u8>, FusionStateCodecError> {
SerializableFusionState::from_filter(self)?.encode_versioned()
}
pub fn restore_serializable_state(
&mut self,
state: &SerializableFusionState,
) -> Result<(), FusionStateCodecError> {
let snapshot = state.to_snapshot()?;
let history = state.to_time_sync_history()?;
self.restore_snapshot(&snapshot).map_err(invalid_state)?;
self.time_sync
.restore_history(history)
.map_err(invalid_state)
}
pub fn restore_encoded_state(&mut self, bytes: &[u8]) -> Result<(), FusionStateCodecError> {
let state = SerializableFusionState::decode_versioned(bytes)?;
self.restore_serializable_state(&state)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FusionStateCodecError {
#[error("fusion state payload has invalid magic")]
InvalidMagic,
#[error("fusion state version {version} is not supported")]
UnsupportedVersion {
version: u16,
},
#[error("fusion state payload truncated at {offset}, needed {needed} bytes, got {actual}")]
Truncated {
offset: usize,
needed: usize,
actual: usize,
},
#[error("fusion state checksum expected {expected:#x} but found {found:#x}")]
Checksum {
expected: u64,
found: u64,
},
#[error("fusion state payload has {remaining} trailing bytes")]
TrailingBytes {
remaining: usize,
},
#[error("invalid fusion state payload: {reason}")]
InvalidState {
reason: String,
},
#[error("fusion state JSON error: {message}")]
Json {
message: String,
},
}
fn invalid_state(error: impl core::fmt::Display) -> FusionStateCodecError {
FusionStateCodecError::InvalidState {
reason: error.to_string(),
}
}
fn checked_u32(value: usize) -> Result<u32, FusionStateCodecError> {
u32::try_from(value).map_err(|_| FusionStateCodecError::InvalidState {
reason: "length exceeds u32".to_string(),
})
}
fn validate_history_by_restore(
snapshot: TimeSyncHistorySnapshot,
) -> Result<TimeSyncHistorySnapshot, FusionStateCodecError> {
snapshot.validate().map_err(invalid_state)?;
Ok(snapshot)
}
fn bits3(values: [f64; 3]) -> [F64Bits; 3] {
values.map(F64Bits::from_f64)
}
fn f643(values: [F64Bits; 3]) -> [f64; 3] {
values.map(F64Bits::to_f64)
}
fn bits3x3(values: [[f64; 3]; 3]) -> [[F64Bits; 3]; 3] {
values.map(bits3)
}
fn f643x3(values: [[F64Bits; 3]; 3]) -> [[f64; 3]; 3] {
values.map(f643)
}
fn bits_slice(values: &[f64]) -> Vec<F64Bits> {
values.iter().copied().map(F64Bits::from_f64).collect()
}
fn f64_vec(values: &[F64Bits]) -> Vec<f64> {
values.iter().copied().map(F64Bits::to_f64).collect()
}
fn bits_matrix(values: &[Vec<f64>]) -> Vec<Vec<F64Bits>> {
values.iter().map(|row| bits_slice(row)).collect()
}
fn f64_matrix(values: &[Vec<F64Bits>]) -> Vec<Vec<f64>> {
values.iter().map(|row| f64_vec(row)).collect()
}
fn write_nav(bytes: &mut Vec<u8>, state: &SerializableNavState) {
write_f64(bytes, state.t_j2000_s);
write_f64_array(bytes, &state.position_ecef_m);
write_f64_array(bytes, &state.velocity_ecef_mps);
for row in &state.attitude_body_to_ecef {
write_f64_array(bytes, row);
}
write_f64_array(bytes, &state.accel_bias_mps2);
write_f64_array(bytes, &state.gyro_bias_rps);
}
fn read_nav(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableNavState, FusionStateCodecError> {
Ok(SerializableNavState {
t_j2000_s: read_f64(bytes, cursor, limit)?,
position_ecef_m: read_f64_array(bytes, cursor, limit)?,
velocity_ecef_mps: read_f64_array(bytes, cursor, limit)?,
attitude_body_to_ecef: [
read_f64_array(bytes, cursor, limit)?,
read_f64_array(bytes, cursor, limit)?,
read_f64_array(bytes, cursor, limit)?,
],
accel_bias_mps2: read_f64_array(bytes, cursor, limit)?,
gyro_bias_rps: read_f64_array(bytes, cursor, limit)?,
})
}
fn write_layout(bytes: &mut Vec<u8>, layout: SerializableErrorStateLayout) {
bytes.push(match layout {
SerializableErrorStateLayout::Fifteen => 15,
SerializableErrorStateLayout::TwentyOne => 21,
});
}
fn read_layout(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableErrorStateLayout, FusionStateCodecError> {
match read_u8(bytes, cursor, limit)? {
15 => Ok(SerializableErrorStateLayout::Fifteen),
21 => Ok(SerializableErrorStateLayout::TwentyOne),
_ => Err(FusionStateCodecError::InvalidState {
reason: "invalid error-state layout tag".to_string(),
}),
}
}
fn write_time_sync_history(
bytes: &mut Vec<u8>,
history: &SerializableTimeSyncHistory,
) -> Result<(), FusionStateCodecError> {
write_u32_checked(bytes, history.config.imu_capacity as usize)?;
write_u32_checked(bytes, history.config.checkpoint_capacity as usize)?;
write_u32_checked(bytes, history.imu_samples.len())?;
for sample in &history.imu_samples {
write_stored_imu_sample(bytes, sample);
}
write_u32_checked(bytes, history.checkpoints.len())?;
for checkpoint in &history.checkpoints {
write_stored_checkpoint(bytes, checkpoint)?;
}
write_u32_checked(bytes, history.measurements.len())?;
for measurement in &history.measurements {
write_stored_measurement(bytes, measurement)?;
}
Ok(())
}
fn read_time_sync_history(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableTimeSyncHistory, FusionStateCodecError> {
let config = SerializableTimeSyncHistoryConfig {
imu_capacity: read_u32(bytes, cursor, limit)?,
checkpoint_capacity: read_u32(bytes, cursor, limit)?,
};
let imu_len = read_len(bytes, cursor, limit, 1, "imu_samples")?;
let mut imu_samples = Vec::with_capacity(imu_len);
for _ in 0..imu_len {
imu_samples.push(read_stored_imu_sample(bytes, cursor, limit)?);
}
let checkpoint_len = read_len(bytes, cursor, limit, 1, "checkpoints")?;
let mut checkpoints = Vec::with_capacity(checkpoint_len);
for _ in 0..checkpoint_len {
checkpoints.push(read_stored_checkpoint(bytes, cursor, limit)?);
}
let measurement_len = read_len(bytes, cursor, limit, 1, "gnss_measurements")?;
let mut measurements = Vec::with_capacity(measurement_len);
for _ in 0..measurement_len {
measurements.push(read_stored_measurement(bytes, cursor, limit)?);
}
let history = SerializableTimeSyncHistory {
config,
imu_samples,
checkpoints,
measurements,
};
history.to_native()?;
Ok(history)
}
fn write_stored_imu_sample(bytes: &mut Vec<u8>, sample: &SerializableStoredImuSample) {
write_f64(bytes, sample.previous_t_j2000_s);
write_imu_sample(bytes, &sample.sample);
match sample.previous_rate {
Some(endpoint) => {
write_bool(bytes, true);
write_rate_endpoint(bytes, endpoint);
}
None => write_bool(bytes, false),
}
}
fn read_stored_imu_sample(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableStoredImuSample, FusionStateCodecError> {
let previous_t_j2000_s = read_f64(bytes, cursor, limit)?;
let sample = read_imu_sample(bytes, cursor, limit)?;
let previous_rate = if read_bool(bytes, cursor, limit)? {
Some(read_rate_endpoint(bytes, cursor, limit)?)
} else {
None
};
Ok(SerializableStoredImuSample {
previous_t_j2000_s,
sample,
previous_rate,
})
}
fn write_imu_sample(bytes: &mut Vec<u8>, sample: &SerializableImuSample) {
write_f64(bytes, sample.t_j2000_s);
match sample.kind {
SerializableImuSampleKind::Rate {
specific_force_mps2,
angular_rate_rps,
} => {
bytes.push(0);
write_f64_array(bytes, &specific_force_mps2);
write_f64_array(bytes, &angular_rate_rps);
}
SerializableImuSampleKind::Increment {
delta_velocity_mps,
delta_theta_rad,
dt_s,
} => {
bytes.push(1);
write_f64_array(bytes, &delta_velocity_mps);
write_f64_array(bytes, &delta_theta_rad);
write_f64(bytes, dt_s);
}
}
}
fn read_imu_sample(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableImuSample, FusionStateCodecError> {
let t_j2000_s = read_f64(bytes, cursor, limit)?;
let kind = match read_u8(bytes, cursor, limit)? {
0 => SerializableImuSampleKind::Rate {
specific_force_mps2: read_f64_array(bytes, cursor, limit)?,
angular_rate_rps: read_f64_array(bytes, cursor, limit)?,
},
1 => SerializableImuSampleKind::Increment {
delta_velocity_mps: read_f64_array(bytes, cursor, limit)?,
delta_theta_rad: read_f64_array(bytes, cursor, limit)?,
dt_s: read_f64(bytes, cursor, limit)?,
},
_ => {
return Err(FusionStateCodecError::InvalidState {
reason: "invalid IMU sample kind tag".to_string(),
});
}
};
Ok(SerializableImuSample { t_j2000_s, kind })
}
fn write_rate_endpoint(bytes: &mut Vec<u8>, endpoint: SerializableRateEndpoint) {
write_f64(bytes, endpoint.t_j2000_s);
write_f64_array(bytes, &endpoint.specific_force_mps2);
write_f64_array(bytes, &endpoint.angular_rate_rps);
}
fn read_rate_endpoint(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableRateEndpoint, FusionStateCodecError> {
Ok(SerializableRateEndpoint {
t_j2000_s: read_f64(bytes, cursor, limit)?,
specific_force_mps2: read_f64_array(bytes, cursor, limit)?,
angular_rate_rps: read_f64_array(bytes, cursor, limit)?,
})
}
fn write_stored_checkpoint(
bytes: &mut Vec<u8>,
checkpoint: &SerializableStoredCheckpoint,
) -> Result<(), FusionStateCodecError> {
write_f64(bytes, checkpoint.t_j2000_s);
write_fusion_snapshot(bytes, checkpoint.snapshot.as_ref())
}
fn read_stored_checkpoint(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableStoredCheckpoint, FusionStateCodecError> {
Ok(SerializableStoredCheckpoint {
t_j2000_s: read_f64(bytes, cursor, limit)?,
snapshot: Box::new(read_fusion_snapshot(bytes, cursor, limit)?),
})
}
fn write_fusion_snapshot(
bytes: &mut Vec<u8>,
snapshot: &SerializableFusionSnapshot,
) -> Result<(), FusionStateCodecError> {
write_layout(bytes, snapshot.state.layout);
write_nav(bytes, &snapshot.state.nominal);
write_f64_vec(bytes, &snapshot.state.error_state)?;
write_f64_matrix(bytes, &snapshot.state.covariance)?;
write_f64_array(bytes, &snapshot.state.accel_scale_factor);
write_f64_array(bytes, &snapshot.state.gyro_scale_factor);
write_f64_array(bytes, &snapshot.last_body_rate_wrt_ecef_rps);
write_f64(bytes, snapshot.tight.clock_bias_m);
write_f64(bytes, snapshot.tight.clock_drift_m_s);
write_f64_matrix(bytes, &snapshot.tight.augmented_covariance)
}
fn read_fusion_snapshot(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableFusionSnapshot, FusionStateCodecError> {
Ok(SerializableFusionSnapshot {
state: SerializableInsFilterState {
layout: read_layout(bytes, cursor, limit)?,
nominal: read_nav(bytes, cursor, limit)?,
error_state: read_f64_vec(bytes, cursor, limit)?,
covariance: read_f64_matrix(bytes, cursor, limit)?,
accel_scale_factor: read_f64_array(bytes, cursor, limit)?,
gyro_scale_factor: read_f64_array(bytes, cursor, limit)?,
},
last_body_rate_wrt_ecef_rps: read_f64_array(bytes, cursor, limit)?,
tight: SerializableTightFilterState {
clock_bias_m: read_f64(bytes, cursor, limit)?,
clock_drift_m_s: read_f64(bytes, cursor, limit)?,
augmented_covariance: read_f64_matrix(bytes, cursor, limit)?,
},
})
}
fn write_stored_measurement(
bytes: &mut Vec<u8>,
measurement: &SerializableStoredGnssMeasurement,
) -> Result<(), FusionStateCodecError> {
match measurement {
SerializableStoredGnssMeasurement::Loose(measurement) => {
bytes.push(0);
write_loose_measurement(bytes, measurement)
}
SerializableStoredGnssMeasurement::Tight(epoch) => {
bytes.push(1);
write_tight_epoch(bytes, epoch)
}
}
}
fn read_stored_measurement(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableStoredGnssMeasurement, FusionStateCodecError> {
match read_u8(bytes, cursor, limit)? {
0 => Ok(SerializableStoredGnssMeasurement::Loose(
read_loose_measurement(bytes, cursor, limit)?,
)),
1 => Ok(SerializableStoredGnssMeasurement::Tight(read_tight_epoch(
bytes, cursor, limit,
)?)),
_ => Err(FusionStateCodecError::InvalidState {
reason: "invalid GNSS measurement tag".to_string(),
}),
}
}
fn write_loose_measurement(
bytes: &mut Vec<u8>,
measurement: &SerializableLooseMeasurement,
) -> Result<(), FusionStateCodecError> {
write_f64(bytes, measurement.t_j2000_s);
write_f64_array(bytes, &measurement.position_ecef_m);
match measurement.velocity_ecef_mps {
Some(velocity) => {
write_bool(bytes, true);
write_f64_array(bytes, &velocity);
}
None => write_bool(bytes, false),
}
write_f64_matrix(bytes, &measurement.covariance)?;
write_u32_checked(bytes, measurement.satellites_used as usize)?;
write_bool(bytes, measurement.solution_valid);
Ok(())
}
fn read_loose_measurement(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableLooseMeasurement, FusionStateCodecError> {
let t_j2000_s = read_f64(bytes, cursor, limit)?;
let position_ecef_m = read_f64_array(bytes, cursor, limit)?;
let velocity_ecef_mps = if read_bool(bytes, cursor, limit)? {
Some(read_f64_array(bytes, cursor, limit)?)
} else {
None
};
Ok(SerializableLooseMeasurement {
t_j2000_s,
position_ecef_m,
velocity_ecef_mps,
covariance: read_f64_matrix(bytes, cursor, limit)?,
satellites_used: read_u32(bytes, cursor, limit)?,
solution_valid: read_bool(bytes, cursor, limit)?,
})
}
fn write_tight_epoch(
bytes: &mut Vec<u8>,
epoch: &SerializableTightGnssEpoch,
) -> Result<(), FusionStateCodecError> {
write_f64(bytes, epoch.t_j2000_s);
write_u32_checked(bytes, epoch.observations.len())?;
for observation in &epoch.observations {
write_tight_observation(bytes, observation);
}
Ok(())
}
fn read_tight_epoch(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableTightGnssEpoch, FusionStateCodecError> {
let t_j2000_s = read_f64(bytes, cursor, limit)?;
let len = read_len(bytes, cursor, limit, 1, "tight_observations")?;
let mut observations = Vec::with_capacity(len);
for _ in 0..len {
observations.push(read_tight_observation(bytes, cursor, limit)?);
}
Ok(SerializableTightGnssEpoch {
t_j2000_s,
observations,
})
}
fn write_tight_observation(bytes: &mut Vec<u8>, observation: &SerializableTightGnssObservation) {
write_satellite_id(bytes, observation.satellite_id);
write_f64(bytes, observation.pseudorange_m);
write_f64(bytes, observation.pseudorange_sigma_m);
match observation.range_rate {
Some(range_rate) => {
write_bool(bytes, true);
write_range_rate_observation(bytes, range_rate);
}
None => write_bool(bytes, false),
}
match observation.carrier_phase {
Some(carrier_phase) => {
write_bool(bytes, true);
write_carrier_phase_observation(bytes, carrier_phase);
}
None => write_bool(bytes, false),
}
write_f64(bytes, observation.ionosphere_delay_m);
write_f64(bytes, observation.troposphere_delay_m);
}
fn read_tight_observation(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableTightGnssObservation, FusionStateCodecError> {
let satellite_id = read_satellite_id(bytes, cursor, limit)?;
let pseudorange_m = read_f64(bytes, cursor, limit)?;
let pseudorange_sigma_m = read_f64(bytes, cursor, limit)?;
let range_rate = if read_bool(bytes, cursor, limit)? {
Some(read_range_rate_observation(bytes, cursor, limit)?)
} else {
None
};
let carrier_phase = if read_bool(bytes, cursor, limit)? {
Some(read_carrier_phase_observation(bytes, cursor, limit)?)
} else {
None
};
Ok(SerializableTightGnssObservation {
satellite_id,
pseudorange_m,
pseudorange_sigma_m,
range_rate,
carrier_phase,
ionosphere_delay_m: read_f64(bytes, cursor, limit)?,
troposphere_delay_m: read_f64(bytes, cursor, limit)?,
})
}
fn write_range_rate_observation(
bytes: &mut Vec<u8>,
observation: SerializableTightRangeRateObservation,
) {
write_f64(bytes, observation.measured_range_rate_m_s);
write_f64(bytes, observation.sigma_m_s);
write_f64(bytes, observation.satellite_clock_drift_m_s);
}
fn read_range_rate_observation(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableTightRangeRateObservation, FusionStateCodecError> {
Ok(SerializableTightRangeRateObservation {
measured_range_rate_m_s: read_f64(bytes, cursor, limit)?,
sigma_m_s: read_f64(bytes, cursor, limit)?,
satellite_clock_drift_m_s: read_f64(bytes, cursor, limit)?,
})
}
fn write_carrier_phase_observation(
bytes: &mut Vec<u8>,
observation: SerializableTightCarrierPhaseObservation,
) {
write_f64(bytes, observation.phase_range_m);
write_f64(bytes, observation.sigma_m);
write_f64(bytes, observation.float_ambiguity_m);
}
fn read_carrier_phase_observation(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableTightCarrierPhaseObservation, FusionStateCodecError> {
Ok(SerializableTightCarrierPhaseObservation {
phase_range_m: read_f64(bytes, cursor, limit)?,
sigma_m: read_f64(bytes, cursor, limit)?,
float_ambiguity_m: read_f64(bytes, cursor, limit)?,
})
}
fn write_satellite_id(bytes: &mut Vec<u8>, id: SerializableSatelliteId) {
bytes.push(match id.system {
GnssSystem::Gps => 0,
GnssSystem::Glonass => 1,
GnssSystem::Galileo => 2,
GnssSystem::BeiDou => 3,
GnssSystem::Qzss => 4,
GnssSystem::Navic => 5,
GnssSystem::Sbas => 6,
});
bytes.push(id.prn);
}
fn read_satellite_id(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<SerializableSatelliteId, FusionStateCodecError> {
let system = match read_u8(bytes, cursor, limit)? {
0 => GnssSystem::Gps,
1 => GnssSystem::Glonass,
2 => GnssSystem::Galileo,
3 => GnssSystem::BeiDou,
4 => GnssSystem::Qzss,
5 => GnssSystem::Navic,
6 => GnssSystem::Sbas,
_ => {
return Err(FusionStateCodecError::InvalidState {
reason: "invalid GNSS system tag".to_string(),
});
}
};
Ok(SerializableSatelliteId {
system,
prn: read_u8(bytes, cursor, limit)?,
})
}
fn write_f64_matrix(
bytes: &mut Vec<u8>,
matrix: &[Vec<F64Bits>],
) -> Result<(), FusionStateCodecError> {
write_u32_checked(bytes, matrix.len())?;
let cols = matrix.first().map_or(0, Vec::len);
write_u32_checked(bytes, cols)?;
for row in matrix {
if row.len() != cols {
return Err(FusionStateCodecError::InvalidState {
reason: "ragged matrix cannot be encoded".to_string(),
});
}
write_f64_vec_body(bytes, row);
}
Ok(())
}
fn read_f64_matrix(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<Vec<Vec<F64Bits>>, FusionStateCodecError> {
let rows = read_u32(bytes, cursor, limit)? as usize;
let cols = read_u32(bytes, cursor, limit)? as usize;
if rows == 0 || cols == 0 {
return Err(FusionStateCodecError::InvalidState {
reason: "matrix dimensions must be positive".to_string(),
});
}
let count = rows
.checked_mul(cols)
.ok_or_else(|| FusionStateCodecError::InvalidState {
reason: "matrix dimensions overflow usize".to_string(),
})?;
let needed = count
.checked_mul(8)
.ok_or_else(|| FusionStateCodecError::InvalidState {
reason: "matrix byte length overflows usize".to_string(),
})?;
ensure_available(*cursor, needed, limit)?;
let mut matrix = Vec::with_capacity(rows);
for _ in 0..rows {
let mut row = Vec::with_capacity(cols);
for _ in 0..cols {
row.push(read_f64(bytes, cursor, limit)?);
}
matrix.push(row);
}
Ok(matrix)
}
fn write_f64_vec(bytes: &mut Vec<u8>, values: &[F64Bits]) -> Result<(), FusionStateCodecError> {
write_u32_checked(bytes, values.len())?;
write_f64_vec_body(bytes, values);
Ok(())
}
fn write_f64_vec_body(bytes: &mut Vec<u8>, values: &[F64Bits]) {
for value in values {
write_f64(bytes, *value);
}
}
fn read_f64_vec(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<Vec<F64Bits>, FusionStateCodecError> {
let len = read_u32(bytes, cursor, limit)? as usize;
let needed = len
.checked_mul(8)
.ok_or_else(|| FusionStateCodecError::InvalidState {
reason: "vector byte length overflows usize".to_string(),
})?;
ensure_available(*cursor, needed, limit)?;
let mut values = Vec::with_capacity(len);
for _ in 0..len {
values.push(read_f64(bytes, cursor, limit)?);
}
Ok(values)
}
fn write_f64_array<const N: usize>(bytes: &mut Vec<u8>, values: &[F64Bits; N]) {
for value in values {
write_f64(bytes, *value);
}
}
fn read_f64_array<const N: usize>(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<[F64Bits; N], FusionStateCodecError> {
let mut out = [F64Bits { bits: 0 }; N];
for value in &mut out {
*value = read_f64(bytes, cursor, limit)?;
}
Ok(out)
}
fn write_f64(bytes: &mut Vec<u8>, value: F64Bits) {
write_u64(bytes, value.bits);
}
fn read_f64(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<F64Bits, FusionStateCodecError> {
Ok(F64Bits {
bits: read_u64(bytes, cursor, limit)?,
})
}
fn write_u16(bytes: &mut Vec<u8>, value: u16) {
bytes.extend_from_slice(&value.to_le_bytes());
}
fn write_u32_checked(bytes: &mut Vec<u8>, value: usize) -> Result<(), FusionStateCodecError> {
let value = u32::try_from(value).map_err(|_| FusionStateCodecError::InvalidState {
reason: "length exceeds u32".to_string(),
})?;
bytes.extend_from_slice(&value.to_le_bytes());
Ok(())
}
fn write_u64(bytes: &mut Vec<u8>, value: u64) {
bytes.extend_from_slice(&value.to_le_bytes());
}
fn write_bool(bytes: &mut Vec<u8>, value: bool) {
bytes.push(u8::from(value));
}
fn read_bool(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
) -> Result<bool, FusionStateCodecError> {
match read_u8(bytes, cursor, limit)? {
0 => Ok(false),
1 => Ok(true),
_ => Err(FusionStateCodecError::InvalidState {
reason: "invalid boolean tag".to_string(),
}),
}
}
fn read_u8(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u8, FusionStateCodecError> {
ensure_available(*cursor, 1, limit)?;
let value = bytes[*cursor];
*cursor += 1;
Ok(value)
}
fn read_u16(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u16, FusionStateCodecError> {
let data = read_array::<2>(bytes, *cursor, limit)?;
*cursor += 2;
Ok(u16::from_le_bytes(data))
}
fn read_u32(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u32, FusionStateCodecError> {
let data = read_array::<4>(bytes, *cursor, limit)?;
*cursor += 4;
Ok(u32::from_le_bytes(data))
}
fn read_u64(bytes: &[u8], cursor: &mut usize, limit: usize) -> Result<u64, FusionStateCodecError> {
let data = read_array::<8>(bytes, *cursor, limit)?;
*cursor += 8;
Ok(u64::from_le_bytes(data))
}
fn read_u64_at(bytes: &[u8], offset: usize) -> Result<u64, FusionStateCodecError> {
Ok(u64::from_le_bytes(read_array::<8>(
bytes,
offset,
bytes.len(),
)?))
}
fn read_len(
bytes: &[u8],
cursor: &mut usize,
limit: usize,
min_element_bytes: usize,
field: &'static str,
) -> Result<usize, FusionStateCodecError> {
let len = read_u32(bytes, cursor, limit)? as usize;
let needed =
len.checked_mul(min_element_bytes)
.ok_or_else(|| FusionStateCodecError::InvalidState {
reason: format!("{field} byte length overflows usize"),
})?;
ensure_available(*cursor, needed, limit)?;
Ok(len)
}
fn read_array<const N: usize>(
bytes: &[u8],
offset: usize,
limit: usize,
) -> Result<[u8; N], FusionStateCodecError> {
ensure_available(offset, N, limit)?;
let end = offset + N;
let mut out = [0u8; N];
out.copy_from_slice(&bytes[offset..end]);
Ok(out)
}
fn ensure_available(
offset: usize,
needed: usize,
limit: usize,
) -> Result<(), FusionStateCodecError> {
let end = offset
.checked_add(needed)
.ok_or(FusionStateCodecError::Truncated {
offset,
needed,
actual: limit.saturating_sub(offset),
})?;
if end <= limit {
Ok(())
} else {
Err(FusionStateCodecError::Truncated {
offset,
needed,
actual: limit.saturating_sub(offset),
})
}
}
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::constants::earth::WGS84_A_M;
use crate::fusion::state::{ErrorStateLayout, ERROR_STATE_DIMENSION_15};
use crate::fusion::TimeSyncHistoryConfig;
use crate::inertial::config::RANDOM_WALK_BIAS_TAU_S;
use crate::inertial::state::mat3_identity;
use crate::inertial::{ImuSample, ImuSpec, NavState};
fn test_filter() -> InertialFilter {
let nominal = NavState::new(
12.5,
[WGS84_A_M, -0.0, 3.25],
[0.5, -0.25, 0.125],
mat3_identity(),
)
.expect("nominal")
.with_biases([0.01, -0.02, 0.03], [-0.001, 0.002, -0.003])
.expect("biases");
let mut diagonal = vec![1.0; ERROR_STATE_DIMENSION_15];
diagonal[0] = 4.0;
diagonal[1] = 9.0;
let state = InsFilterState::from_diagonal(nominal, ErrorStateLayout::Fifteen, &diagonal)
.expect("state");
let spec = ImuSpec::datasheet(
0.0,
0.0,
0.0,
0.0,
RANDOM_WALK_BIAS_TAU_S,
RANDOM_WALK_BIAS_TAU_S,
None,
None,
);
InertialFilter::new(state, spec).expect("filter")
}
fn increment(t_j2000_s: f64, dt_s: f64) -> ImuSample {
ImuSample::increment(
t_j2000_s,
[0.015625 * dt_s, -0.0078125 * dt_s, 0.00390625 * dt_s],
[
0.0009765625 * dt_s,
-0.00048828125 * dt_s,
0.000244140625 * dt_s,
],
dt_s,
)
}
fn measurement_at(t_j2000_s: f64, position_ecef_m: [f64; 3]) -> GnssFixMeasurement {
GnssFixMeasurement::position(
t_j2000_s,
position_ecef_m,
[[4.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 6.0]],
8,
)
.expect("measurement")
}
#[test]
fn binary_and_json_round_trip_preserve_bits() {
let filter = test_filter();
let serial = filter.serializable_state().expect("serial state");
let encoded = serial.encode_versioned().expect("encode");
let decoded = SerializableFusionState::decode_versioned(&encoded).expect("decode");
assert_eq!(decoded, serial);
assert_snapshot_bits(
&decoded.to_snapshot().expect("snapshot"),
&filter.snapshot(),
);
let json = serial.to_json_string().expect("json");
let decoded_json = SerializableFusionState::from_json_str(&json).expect("json decode");
assert_eq!(decoded_json, serial);
assert_snapshot_bits(
&decoded_json.to_snapshot().expect("json snapshot"),
&filter.snapshot(),
);
}
#[test]
fn truncated_and_corrupted_payloads_are_typed_errors() {
let serial = test_filter().serializable_state().expect("serial state");
let encoded = serial.encode_versioned().expect("encode");
let truncated = &encoded[..encoded.len() - 3];
assert!(matches!(
SerializableFusionState::decode_versioned(truncated),
Err(FusionStateCodecError::Checksum { .. })
));
let mut corrupted = encoded;
let idx = corrupted.len() / 2;
corrupted[idx] ^= 0x55;
assert!(matches!(
SerializableFusionState::decode_versioned(&corrupted),
Err(FusionStateCodecError::Checksum { .. })
));
let too_short = [0u8; 5];
assert!(matches!(
SerializableFusionState::decode_versioned(&too_short),
Err(FusionStateCodecError::Truncated { .. })
));
}
#[test]
fn malformed_matrix_dimensions_are_typed_errors() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&u32::MAX.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
let mut cursor = 0usize;
assert!(matches!(
read_f64_matrix(&bytes, &mut cursor, bytes.len()),
Err(FusionStateCodecError::InvalidState { .. })
));
}
#[test]
fn restored_encoded_state_retains_time_sync_history_for_late_replay_bits() {
let first = measurement_at(13.0, [WGS84_A_M + 0.25, -0.125, 3.5]);
let late = measurement_at(13.25, [WGS84_A_M - 0.0625, 0.1875, 3.0]);
let final_fix = measurement_at(13.5, [WGS84_A_M + 0.03125, -0.25, 3.125]);
let mut direct = test_filter();
direct
.configure_time_sync_history(TimeSyncHistoryConfig::new(8, 8))
.expect("history");
direct.propagate(increment(13.0, 0.5)).expect("imu");
direct.update_loose(&first).expect("first");
direct.propagate(increment(13.25, 0.25)).expect("imu");
direct.update_loose(&late).expect("late in order");
direct.propagate(increment(13.5, 0.25)).expect("imu");
direct.update_loose(&final_fix).expect("final");
direct.propagate(increment(14.0, 0.5)).expect("imu");
let mut delayed = test_filter();
delayed
.configure_time_sync_history(TimeSyncHistoryConfig::new(8, 8))
.expect("history");
delayed.propagate(increment(13.0, 0.5)).expect("imu");
delayed.update_loose(&first).expect("first");
delayed.propagate(increment(13.5, 0.5)).expect("imu");
delayed.update_loose(&final_fix).expect("final");
delayed.propagate(increment(14.0, 0.5)).expect("imu");
let encoded = delayed.encode_state().expect("encode");
let mut restored = test_filter();
restored.restore_encoded_state(&encoded).expect("restore");
let update = restored
.update_loose_time_sync(&late)
.expect("late replay after restore");
assert!(update.late_measurement);
assert_snapshot_bits(&restored.snapshot(), &direct.snapshot());
}
fn assert_snapshot_bits(actual: &InertialFilterSnapshot, expected: &InertialFilterSnapshot) {
assert_eq!(
actual.state.nominal.t_j2000_s.to_bits(),
expected.state.nominal.t_j2000_s.to_bits()
);
for axis in 0..3 {
assert_eq!(
actual.state.nominal.position_ecef_m[axis].to_bits(),
expected.state.nominal.position_ecef_m[axis].to_bits()
);
assert_eq!(
actual.last_body_rate_wrt_ecef_rps[axis].to_bits(),
expected.last_body_rate_wrt_ecef_rps[axis].to_bits()
);
}
for row in 0..actual.state.covariance.len() {
for col in 0..actual.state.covariance[row].len() {
assert_eq!(
actual.state.covariance[row][col].to_bits(),
expected.state.covariance[row][col].to_bits()
);
}
}
assert_eq!(
actual.tight.clock_bias_m.to_bits(),
expected.tight.clock_bias_m.to_bits()
);
assert_eq!(
actual.tight.clock_drift_m_s.to_bits(),
expected.tight.clock_drift_m_s.to_bits()
);
for row in 0..actual.tight.augmented_covariance.len() {
for col in 0..actual.tight.augmented_covariance[row].len() {
assert_eq!(
actual.tight.augmented_covariance[row][col].to_bits(),
expected.tight.augmented_covariance[row][col].to_bits()
);
}
}
}
}