Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
165 lines
5.9 KiB
Rust
165 lines
5.9 KiB
Rust
mod support;
|
|
use kaptan_config::ConfigModel;
|
|
use kaptan_flight_runtime::*;
|
|
use kaptan_math::Vector3;
|
|
use kaptan_navigation::{
|
|
AccelerationFrd, AngularRateFrd, NavigationFailure, PositionNed, SensorRecord, VelocityNed,
|
|
};
|
|
use support::*;
|
|
|
|
#[test]
|
|
fn actual_navigation_fixed_reference_origin_and_delayed_aiding() {
|
|
let (ep, out, now) = ready();
|
|
let estimate = ep.status().estimate.unwrap();
|
|
assert!(
|
|
estimate.capabilities.flight_ready(),
|
|
"{:?}",
|
|
estimate.capabilities
|
|
);
|
|
assert!(estimate.position.vector().norm() < 0.01);
|
|
assert!((estimate.origin.launch_in_reference.vector().x() - 12.).abs() < 0.001);
|
|
assert!(estimate.fused_through.as_microseconds() <= now - 100_000);
|
|
assert_eq!(estimate.at, time(now));
|
|
assert!(
|
|
estimate
|
|
.ground_clearance
|
|
.unwrap()
|
|
.contact_clearance_metres
|
|
.abs()
|
|
< 0.002
|
|
);
|
|
assert!(out.outputs.iter().all(|v| !v.permitted()));
|
|
}
|
|
#[test]
|
|
fn navigation_failures_retain_their_reason() {
|
|
for (delay, observe, failure, reason) in [
|
|
(
|
|
30_000,
|
|
true,
|
|
NavigationFailure::ImuGap,
|
|
TerminalReason::ImuLost,
|
|
),
|
|
(
|
|
100_000,
|
|
false,
|
|
NavigationFailure::WorkBudget,
|
|
TerminalReason::NavigationLost,
|
|
),
|
|
] {
|
|
let (mut ep, mut out, now) = ready();
|
|
let now = now + delay;
|
|
let error = if observe {
|
|
ep.observe(
|
|
ep.status().epoch,
|
|
SensorRecord::Imu(imu(now)),
|
|
time(now),
|
|
&mut out,
|
|
)
|
|
.unwrap_err()
|
|
} else {
|
|
ep.tick(time(now), &mut out).unwrap_err()
|
|
};
|
|
assert_eq!(error.primary, EndpointFailure::Navigation(failure));
|
|
assert_eq!(ep.status().terminal_reason, Some(reason));
|
|
assert_eq!(ep.status().phase, VehiclePhase::Terminated);
|
|
assert!(!out.outputs.last().unwrap().permitted());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn healthy_armed_idle_expires_without_rearming() {
|
|
let (mut ep, mut out, now) = armed();
|
|
for t in (now + 10_000..=now + 10_500_000).step_by(10_000) {
|
|
feed(&mut ep, &mut out, t);
|
|
}
|
|
assert_eq!(ep.status().phase, VehiclePhase::Disarmed);
|
|
assert_eq!(ep.status().arm, None);
|
|
assert!(out.outputs.iter().all(|v| !v.permitted()));
|
|
}
|
|
|
|
#[test]
|
|
fn declared_noise_realizations_initialize_and_remain_grounded() {
|
|
// Box-Muller Gaussian draws, independently implemented at the sensor boundary.
|
|
// q/T gives IMU per-sample variance; no packet carries an ImuNoise value.
|
|
let mut seed = 0xaca5_8113_78ab_1234_u64;
|
|
let mut uniform = || {
|
|
seed ^= seed << 13;
|
|
seed ^= seed >> 7;
|
|
seed ^= seed << 17;
|
|
((seed >> 11) as f64 + 0.5) / 9_007_199_254_740_992.
|
|
};
|
|
let mut normal = || (-2. * uniform().ln()).sqrt() * (core::f64::consts::TAU * uniform()).cos();
|
|
let profile = profile_fixture::candidate()
|
|
.validate(&profile_fixture::capabilities())
|
|
.unwrap();
|
|
let mut ep = FlightEndpoint::new(
|
|
profile,
|
|
RunBinding {
|
|
session: TransportSessionId(7),
|
|
owner: CommandOwner(1),
|
|
},
|
|
)
|
|
.unwrap();
|
|
let mut out = Recording::default();
|
|
for m in (10_000..=6_000_000).step_by(10_000) {
|
|
let t = m + 1_000;
|
|
for mut record in records(t) {
|
|
match &mut record {
|
|
SensorRecord::Imu(r) => {
|
|
let s = (2.5e-5_f64 / 0.01).sqrt();
|
|
r.acceleration = AccelerationFrd::metres_per_second_squared(
|
|
(normal() * s) as f32,
|
|
(normal() * s) as f32,
|
|
(-f64::from(kaptan_navigation::GRAVITY_MPS2) + normal() * s) as f32,
|
|
);
|
|
let s = (2.5e-7_f64 / 0.01).sqrt();
|
|
r.angular_rate = AngularRateFrd::radians_per_second(
|
|
(normal() * s) as f32,
|
|
(normal() * s) as f32,
|
|
(normal() * s) as f32,
|
|
);
|
|
}
|
|
SensorRecord::LocalPositionVelocity(r) => {
|
|
r.position = PositionNed::metres(
|
|
(12. + normal() * 0.1) as f32,
|
|
(-7. + normal() * 0.1) as f32,
|
|
(4. + normal() * 0.1) as f32,
|
|
);
|
|
r.velocity = VelocityNed::metres_per_second(
|
|
(normal() * 0.05) as f32,
|
|
(normal() * 0.05) as f32,
|
|
(normal() * 0.05) as f32,
|
|
);
|
|
}
|
|
SensorRecord::Magnetic(r) => {
|
|
r.microtesla_frd = Vector3::new(
|
|
(20. + normal() * 0.02) as f32,
|
|
(normal() * 0.02) as f32,
|
|
(45. + normal() * 0.02) as f32,
|
|
)
|
|
}
|
|
SensorRecord::Pressure(r) => r.pascals = (101_325. + normal() * 0.3) as f32,
|
|
SensorRecord::Range(r) => {
|
|
r.metres = (((0.04 + normal() * 0.002) / 0.001).round() * 0.001) as f32
|
|
}
|
|
}
|
|
let admitted = ep
|
|
.observe(ep.status().epoch, record, time(t), &mut out)
|
|
.unwrap();
|
|
assert!(
|
|
admitted == ObservationAdmission::Queued
|
|
|| admitted
|
|
== ObservationAdmission::Rejected(
|
|
ObservationRejection::BehindFusionHorizon
|
|
)
|
|
);
|
|
}
|
|
ep.tick(time(t), &mut out).unwrap();
|
|
}
|
|
let e = ep.status().estimate.unwrap();
|
|
assert!(e.capabilities.flight_ready(), "{:?}", e.capabilities);
|
|
assert!(e.position.vector().norm() < 0.1, "{:?}", e.position);
|
|
assert_eq!(ep.status().ground_state, GroundState::Grounded);
|
|
assert!(out.outputs.iter().all(|r| !r.permitted()));
|
|
}
|