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:
Onur Özkan committed 2026-11-07 18:12:11 +03:00
1 parent fb1b8af2d1
commit f9a61a086f
211 files changed
+45657 -12740

No files matched your search

+117 -43
View File
@@ -57,23 +57,31 @@ impl EnqueueResult {
/// Result of attempting to begin a transfer.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StartOutcome {
/// No pending frame was available.
/// No transfer started.
Idle,
/// The engine accepted a frame and owns it until a terminal outcome.
/// The transport accepted a frame, which the queue retains until completion or failure.
Started,
}
/// Bounded admission and exactly-one-active-transfer state.
///
/// Pending best-effort traffic is replaceable; active traffic is never
/// replaceable. A selected frame moves into its final active storage before
/// the engine sees its address. A rejected start moves it back to the
/// reservation, so a failed submission cannot manufacture a false in-flight
/// frame or discard the selected frame.
/// replaceable. The queue retains the selected frame as logically active until
/// completion or failure. A rejected start returns it to the reservation for
/// retry without discarding it or claiming an active transfer.
///
/// The queue is movable and provides no stable address for DMA. Transports may
/// copy bytes during a callback or borrow the active frame for bounded writes.
/// Asynchronous device access requires separately owned, address-stable storage.
pub struct TransmitQueue<Frame, const N: usize> {
pending: [Option<PendingFrame<Frame>>; N],
reserved: Option<PendingFrame<Frame>>,
active: Option<PendingFrame<Frame>>,
selected: SelectedFrame<Frame>,
}
enum SelectedFrame<Frame> {
Idle,
Reserved(PendingFrame<Frame>),
Active(PendingFrame<Frame>),
}
impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
@@ -81,8 +89,7 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
pub const fn new() -> Self {
Self {
pending: [None; N],
reserved: None,
active: None,
selected: SelectedFrame::Idle,
}
}
@@ -123,43 +130,46 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
}
/// Selects and starts at most one pending frame.
///
/// The callback borrows the frame only for this call. Success retains logical
/// active ownership, not an address or a borrow for later device access.
pub fn start_with<Error>(
&mut self,
start: impl FnOnce(&Frame) -> Result<(), Error>,
) -> Result<StartOutcome, Error> {
if self.active.is_some() {
return Ok(StartOutcome::Idle);
}
if self.reserved.is_none() {
self.reserved = self.take_next();
}
self.active = self.reserved.take();
let Some(active) = self.active.as_ref() else {
return Ok(StartOutcome::Idle);
let frame = match self.selected {
SelectedFrame::Active(_) => return Ok(StartOutcome::Idle),
SelectedFrame::Reserved(frame) => frame,
SelectedFrame::Idle => match self.take_next() {
Some(frame) => frame,
None => return Ok(StartOutcome::Idle),
},
};
if let Err(error) = start(&active.frame) {
self.reserved = self.active.take();
self.selected = SelectedFrame::Active(frame);
if let Err(error) = start(&frame.frame) {
self.selected = SelectedFrame::Reserved(frame);
return Err(error);
}
self.reserved = None;
Ok(StartOutcome::Started)
}
/// Releases the active frame after the transport no longer borrows it.
/// Releases the logically active frame after transport completion.
pub fn complete(&mut self) -> Option<PendingFrame<Frame>> {
self.active.take()
let SelectedFrame::Active(frame) = self.selected else {
return None;
};
self.selected = SelectedFrame::Idle;
Some(frame)
}
/// Releases the active frame after a terminal transport fault.
pub fn fail(&mut self) -> Option<PendingFrame<Frame>> {
self.active.take()
self.complete()
}
/// Returns whether an engine owns a frame.
/// Returns whether the queue retains a logically active frame.
pub const fn is_active(&self) -> bool {
self.active.is_some()
matches!(self.selected, SelectedFrame::Active(_))
}
/// Borrows the frame currently owned by the active transport operation.
@@ -167,7 +177,10 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
/// Packetized transports use this to retain one selected frame while
/// advancing it across multiple bounded endpoint writes.
pub const fn active(&self) -> Option<&PendingFrame<Frame>> {
self.active.as_ref()
match &self.selected {
SelectedFrame::Active(frame) => Some(frame),
SelectedFrame::Idle | SelectedFrame::Reserved(_) => None,
}
}
fn take_next(&mut self) -> Option<PendingFrame<Frame>> {
@@ -194,6 +207,8 @@ impl<Frame: Copy, const N: usize> Default for TransmitQueue<Frame, N> {
#[cfg(test)]
mod tests {
extern crate std;
use super::{EnqueueResult, FrameClass, PendingFrame, StartOutcome, TransmitQueue};
#[test]
@@ -352,7 +367,7 @@ mod tests {
}
#[test]
fn rejects_start_without_claiming_active_ownership() {
fn failed_start_retains_selection() {
let mut queue = TransmitQueue::<u8, 1>::new();
queue.enqueue(PendingFrame {
class: FrameClass::BestEffort,
@@ -361,6 +376,16 @@ mod tests {
assert_eq!(queue.start_with(|_| Err::<(), _>(1)), Err(1));
assert!(!queue.is_active());
assert_eq!(queue.active(), None);
assert_eq!(queue.complete(), None);
assert_eq!(queue.fail(), None);
assert_eq!(
queue.enqueue(PendingFrame {
class: FrameClass::Vital,
frame: 8,
}),
EnqueueResult::Enqueued
);
assert_eq!(
queue.start_with(|frame| {
assert_eq!(*frame, 7);
@@ -368,31 +393,80 @@ mod tests {
}),
Ok(StartOutcome::Started)
);
assert_eq!(queue.complete().unwrap().frame, 7);
queue
.start_with(|frame| {
assert_eq!(*frame, 8);
Ok::<_, ()>(())
})
.unwrap();
}
#[test]
fn keeps_the_dma_source_at_its_active_address() {
let mut queue = TransmitQueue::<[u8; 2], 1>::new();
fn panicking_start_retains_active() {
let mut queue = TransmitQueue::<u8, 1>::new();
queue.enqueue(PendingFrame {
class: FrameClass::Critical,
frame: 7,
});
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = queue.start_with::<()>(|_| panic!("transport start panicked"));
}));
assert!(result.is_err());
assert_eq!(queue.active().unwrap().frame, 7);
assert_eq!(
queue.start_with::<()>(|_| panic!("active transfer cannot restart")),
Ok(StartOutcome::Idle)
);
assert_eq!(queue.fail().unwrap().frame, 7);
}
#[test]
fn retains_active_contents_across_pending_admission_and_queue_move() {
let mut queue = TransmitQueue::<[u8; 2], 1>::new();
let active = PendingFrame {
class: FrameClass::BestEffort,
frame: [1, 2],
});
};
queue.enqueue(active);
let mut submitted_address = 0;
assert_eq!(
queue.start_with(|frame| {
submitted_address = frame.as_ptr() as usize;
assert_eq!(*frame, active.frame);
Ok::<_, ()>(())
}),
Ok(StartOutcome::Started)
);
let active_address = queue
.active
.as_ref()
.expect("successful start must retain the active frame")
.frame
.as_ptr() as usize;
assert_eq!(
queue.enqueue(PendingFrame {
class: FrameClass::BestEffort,
frame: [3, 4],
}),
EnqueueResult::Enqueued
);
let pending = PendingFrame {
class: FrameClass::Critical,
frame: [5, 6],
};
assert_eq!(queue.enqueue(pending), EnqueueResult::ReplacedBestEffort);
assert_eq!(queue.active(), Some(&active));
assert_eq!(submitted_address, active_address);
let mut moved = core::hint::black_box(queue);
assert!(moved.is_active());
assert_eq!(moved.active(), Some(&active));
assert_eq!(
moved.start_with(|_| panic!("active frame must finish before another start")),
Ok::<_, ()>(StartOutcome::Idle)
);
assert_eq!(moved.complete(), Some(active));
assert_eq!(moved.active(), None);
assert_eq!(
moved.start_with(|frame| {
assert_eq!(*frame, pending.frame);
Ok::<_, ()>(())
}),
Ok(StartOutcome::Started)
);
assert_eq!(moved.complete(), Some(pending));
}
}