Files
Onur Özkan f9a61a086f 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.
2026-11-07 18:12:11 +03:00

269 lines
7.7 KiB
Rust

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
);
}