feat(firmware): validate configuration and migrate sensor-driven flight APIs
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.
This commit is contained in:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -0,0 +1,164 @@
|
||||
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()));
|
||||
}
|
||||
Reference in new issue
Block a user