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,268 @@
|
||||
use kaptan_safety::*;
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
fn time(value: u64) -> Timestamp {
|
||||
Timestamp::from_microseconds(value)
|
||||
}
|
||||
fn healthy() -> SafetyInputs {
|
||||
SafetyInputs {
|
||||
estimator_healthy: true,
|
||||
imu_fresh: true,
|
||||
phase_authorized: true,
|
||||
output_healthy: true,
|
||||
}
|
||||
}
|
||||
fn supervisor() -> SafetySupervisor {
|
||||
SafetySupervisor::new(SafetyConfig::new(100).unwrap())
|
||||
}
|
||||
fn evaluate(s: &mut SafetySupervisor, now: u64, inputs: SafetyInputs) -> CommitOutcome<(), ()> {
|
||||
s.evaluate_and_commit(time(now), inputs, || Ok(()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dwell_and_owner_local_commit() {
|
||||
assert_eq!(
|
||||
SafetyConfig::new(0),
|
||||
Err(SafetyConfigError::ZeroPrearmDwell)
|
||||
);
|
||||
let mut a = supervisor();
|
||||
let mut b = supervisor();
|
||||
assert_eq!(
|
||||
a.request_arm(time(10), healthy()).state(),
|
||||
SafetyState::PreArm
|
||||
);
|
||||
for now in [10, 109] {
|
||||
assert!(matches!(
|
||||
a.evaluate_and_commit(time(now), healthy(), || -> Result<(), ()> {
|
||||
panic!("prearm cannot commit")
|
||||
}),
|
||||
CommitOutcome::Inhibited(_)
|
||||
));
|
||||
}
|
||||
let mut calls = 0;
|
||||
assert_eq!(
|
||||
a.evaluate_and_commit(time(110), healthy(), || {
|
||||
calls += 1;
|
||||
Ok::<_, ()>(42)
|
||||
}),
|
||||
CommitOutcome::Committed(42)
|
||||
);
|
||||
assert_eq!(calls, 1);
|
||||
// Healthy armed evaluations keep committing without restarting the dwell.
|
||||
assert_eq!(
|
||||
evaluate(&mut a, 111, healthy()),
|
||||
CommitOutcome::Committed(())
|
||||
);
|
||||
assert!(matches!(
|
||||
b.evaluate_and_commit(time(110), healthy(), || -> Result<(), ()> {
|
||||
panic!("another owner's armed state cannot commit here")
|
||||
}),
|
||||
CommitOutcome::Inhibited(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_arm_and_prearm_are_consumed() {
|
||||
let mut s = supervisor();
|
||||
let mut stale = healthy();
|
||||
stale.imu_fresh = false;
|
||||
assert_eq!(
|
||||
s.request_arm(time(1), stale).failure(),
|
||||
Some(SafetyFailure::ImuStale)
|
||||
);
|
||||
evaluate(&mut s, 200, healthy());
|
||||
assert_eq!(s.state(), SafetyState::Disarmed);
|
||||
s.request_arm(time(201), healthy());
|
||||
evaluate(&mut s, 202, stale);
|
||||
evaluate(&mut s, 400, healthy());
|
||||
assert_eq!(s.state(), SafetyState::Disarmed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_arm_health_priority_and_recovery_require_a_new_transaction() {
|
||||
for (inputs, expected) in [
|
||||
(
|
||||
SafetyInputs {
|
||||
estimator_healthy: false,
|
||||
imu_fresh: false,
|
||||
phase_authorized: false,
|
||||
output_healthy: false,
|
||||
},
|
||||
SafetyFailure::ActuatorFault,
|
||||
),
|
||||
(
|
||||
SafetyInputs {
|
||||
estimator_healthy: false,
|
||||
imu_fresh: false,
|
||||
phase_authorized: false,
|
||||
..healthy()
|
||||
},
|
||||
SafetyFailure::EstimatorUnhealthy,
|
||||
),
|
||||
(
|
||||
SafetyInputs {
|
||||
imu_fresh: false,
|
||||
phase_authorized: false,
|
||||
..healthy()
|
||||
},
|
||||
SafetyFailure::ImuStale,
|
||||
),
|
||||
(
|
||||
SafetyInputs {
|
||||
phase_authorized: false,
|
||||
..healthy()
|
||||
},
|
||||
SafetyFailure::PhaseUnauthorized,
|
||||
),
|
||||
] {
|
||||
let mut s = supervisor();
|
||||
let expected_state = if expected == SafetyFailure::ActuatorFault {
|
||||
SafetyState::Failsafe
|
||||
} else {
|
||||
SafetyState::Disarmed
|
||||
};
|
||||
let decision = s.request_arm(time(0), inputs);
|
||||
assert_eq!(decision.failure(), Some(expected));
|
||||
assert_eq!(decision.state(), expected_state);
|
||||
assert!(matches!(
|
||||
evaluate(&mut s, 100, healthy()),
|
||||
CommitOutcome::Inhibited(_)
|
||||
));
|
||||
assert_eq!(s.state(), expected_state);
|
||||
if expected == SafetyFailure::ActuatorFault {
|
||||
assert_eq!(
|
||||
s.request_arm(time(101), healthy()).state(),
|
||||
SafetyState::Failsafe
|
||||
);
|
||||
s.request_disarm();
|
||||
}
|
||||
assert_eq!(
|
||||
s.request_arm(time(102), healthy()).state(),
|
||||
SafetyState::PreArm
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&mut s, 202, healthy()),
|
||||
CommitOutcome::Committed(())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regressing_arm_request_consumes_prearm_and_requires_explicit_reset() {
|
||||
let mut s = supervisor();
|
||||
assert_eq!(
|
||||
s.request_arm(time(100), healthy()).state(),
|
||||
SafetyState::PreArm
|
||||
);
|
||||
let decision = s.request_arm(time(99), healthy());
|
||||
assert_eq!(decision.state(), SafetyState::Failsafe);
|
||||
assert_eq!(decision.failure(), Some(SafetyFailure::ClockRegression));
|
||||
assert!(matches!(
|
||||
evaluate(&mut s, 200, healthy()),
|
||||
CommitOutcome::Inhibited(_)
|
||||
));
|
||||
s.request_disarm();
|
||||
s.reset_time_epoch(|| Ok::<_, ()>(())).unwrap();
|
||||
assert_eq!(
|
||||
s.request_arm(time(0), healthy()).state(),
|
||||
SafetyState::PreArm
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_health_revokes_before_callback_and_recovery_cannot_rearm() {
|
||||
let mut s = supervisor();
|
||||
s.request_arm(time(0), healthy());
|
||||
assert_eq!(
|
||||
evaluate(&mut s, 100, healthy()),
|
||||
CommitOutcome::Committed(())
|
||||
);
|
||||
let diagnostic = s.decision();
|
||||
let mut stale = healthy();
|
||||
stale.phase_authorized = false;
|
||||
assert!(matches!(
|
||||
s.evaluate_and_commit(time(101), stale, || -> Result<(), ()> {
|
||||
panic!("stale evidence must inhibit before callback")
|
||||
}),
|
||||
CommitOutcome::Inhibited(_)
|
||||
));
|
||||
assert_eq!(diagnostic.state(), SafetyState::Armed); // retained diagnostic only
|
||||
assert_eq!(
|
||||
s.decision().failure(),
|
||||
Some(SafetyFailure::PhaseUnauthorized)
|
||||
);
|
||||
assert!(matches!(
|
||||
evaluate(&mut s, 102, healthy()),
|
||||
CommitOutcome::Inhibited(_)
|
||||
));
|
||||
assert_eq!(
|
||||
s.request_arm(time(103), healthy()).state(),
|
||||
SafetyState::Failsafe
|
||||
);
|
||||
s.request_disarm();
|
||||
assert_eq!(
|
||||
s.request_arm(time(104), healthy()).state(),
|
||||
SafetyState::PreArm
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&mut s, 204, healthy()),
|
||||
CommitOutcome::Committed(())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_error_and_actuator_health_require_disarm() {
|
||||
let mut s = supervisor();
|
||||
s.request_arm(time(0), healthy());
|
||||
assert_eq!(
|
||||
s.evaluate_and_commit(time(100), healthy(), || Err::<(), _>("commit")),
|
||||
CommitOutcome::Failed("commit")
|
||||
);
|
||||
assert_eq!(s.state(), SafetyState::Failsafe);
|
||||
evaluate(&mut s, 101, healthy());
|
||||
assert_eq!(
|
||||
s.request_arm(time(102), healthy()).state(),
|
||||
SafetyState::Failsafe
|
||||
);
|
||||
s.request_disarm();
|
||||
s.request_arm(time(103), healthy());
|
||||
let mut broken = healthy();
|
||||
broken.output_healthy = false;
|
||||
evaluate(&mut s, 104, broken);
|
||||
assert_eq!(s.decision().failure(), Some(SafetyFailure::ActuatorFault));
|
||||
evaluate(&mut s, 300, healthy());
|
||||
assert_eq!(s.state(), SafetyState::Failsafe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_fault_survives_disarm_and_failed_reset() {
|
||||
let mut s = supervisor();
|
||||
evaluate(&mut s, 100, healthy());
|
||||
evaluate(&mut s, 99, healthy());
|
||||
assert_eq!(s.decision().failure(), Some(SafetyFailure::ClockRegression));
|
||||
assert_eq!(
|
||||
s.reset_time_epoch(|| Ok::<_, ()>(())),
|
||||
Err(ResetError::NotDisarmed)
|
||||
);
|
||||
s.request_disarm();
|
||||
assert_eq!(
|
||||
s.request_arm(time(101), healthy()).state(),
|
||||
SafetyState::Failsafe
|
||||
);
|
||||
s.request_disarm();
|
||||
assert_eq!(
|
||||
s.reset_time_epoch(|| Err("inhibit")),
|
||||
Err(ResetError::Inhibit("inhibit"))
|
||||
);
|
||||
s.request_disarm();
|
||||
assert_eq!(
|
||||
s.request_arm(time(102), healthy()).state(),
|
||||
SafetyState::Failsafe
|
||||
);
|
||||
s.request_disarm();
|
||||
s.reset_time_epoch(|| Ok::<_, ()>(())).unwrap();
|
||||
assert_eq!(
|
||||
s.request_arm(time(0), healthy()).state(),
|
||||
SafetyState::PreArm
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user