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

+154 -191
View File
@@ -1,210 +1,146 @@
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
};
use kaptan_mavlink::{
MavlinkFrame,
encoder::MavlinkEncoder,
generated::kaptan::{FILE_DATA_DATA, MavMessage},
};
use kaptan_tx::{FrameClass, PendingFrame};
use usb_device::{
UsbDirection, UsbError,
bus::{PollResult, UsbBus, UsbBusAllocator},
device::{UsbDeviceBuilder, UsbVidPid},
endpoint::{EndpointAddress, EndpointType},
};
use usbd_serial::CdcAcmClass;
use usb_device::bus::PollResult;
#[path = "../src/f722/usb.rs"]
mod usb;
use usb::UsbMavlink;
use usb::test_bus as usb_bus;
use usb_bus::with_link;
#[path = "../src/f722/serial.rs"]
mod serial;
use serial::{IrqTransmit, UartIo};
// One pending packet per endpoint, with host-controlled completion. Device and
// CDC control requests run through the real usb-device/usbd-serial code.
#[derive(Default)]
struct BusState {
events: VecDeque<PollResult>,
input: [Option<Vec<u8>>; 4],
output: [Option<Vec<u8>>; 4],
sizes: [[u16; 4]; 2],
stalled: [[bool; 4]; 2],
bulk_in: usize,
bulk_out: usize,
struct MockUart {
bytes: Vec<u8>,
ready: bool,
complete: bool,
ready_irq: bool,
complete_irq: bool,
}
impl UartIo for MockUart {
fn tx_ready(&mut self) -> bool {
self.ready
}
fn tx_complete(&mut self) -> bool {
self.complete
}
fn write_byte(&mut self, byte: u8) {
self.bytes.push(byte);
self.complete = false;
}
fn interrupts(&mut self, ready: bool, complete: bool) {
self.ready_irq = ready;
self.complete_irq = complete;
}
}
fn flush_uart(tx: &mut IrqTransmit, io: &mut MockUart) {
io.ready = true;
for _ in 0..2048 {
if !io.ready_irq && !io.complete_irq {
return;
}
let previous = io.bytes.len();
io.complete = true;
tx.on_interrupt(io);
assert!(
io.bytes.len() <= previous + 1,
"IRQ work is bounded to one byte"
);
}
panic!("UART did not drain its bounded queue");
}
#[derive(Clone, Default)]
struct Host(Arc<Mutex<BusState>>);
#[test]
fn moving_irq_queue_and_waiting_for_tc_preserves_exact_frame_bytes() {
let mut io = MockUart {
ready: true,
..MockUart::default()
};
let mut tx = IrqTransmit::new();
let mut encoder = MavlinkEncoder::new(1, 1);
let frame = encoder.encode(&kaptan_mavlink::generated::kaptan::HEARTBEAT_DATA::default());
assert!(tx.send(
&mut io,
PendingFrame {
class: FrameClass::Vital,
frame
}
));
assert!(!tx.on_interrupt(&mut io));
let mut moved = tx;
for _ in 1..frame.raw_bytes().len() {
assert!(!moved.on_interrupt(&mut io));
}
assert_eq!(io.bytes, frame.raw_bytes());
assert!(!io.ready_irq && io.complete_irq);
assert!(!moved.on_interrupt(&mut io));
io.complete = true;
assert!(moved.on_interrupt(&mut io));
assert!(!io.ready_irq && !io.complete_irq);
assert!(!moved.on_interrupt(&mut io));
}
struct PacketBus(Arc<Mutex<BusState>>);
impl UsbBus for PacketBus {
fn alloc_ep(
&mut self,
direction: UsbDirection,
address: Option<EndpointAddress>,
kind: EndpointType,
size: u16,
_: u8,
) -> Result<EndpointAddress, UsbError> {
let mut state = self.0.lock().unwrap();
let sizes = &mut state.sizes[usize::from(direction == UsbDirection::In)];
let index = match address {
Some(address) => address.index(),
None => (1..sizes.len())
.find(|index| sizes[*index] == 0)
.ok_or(UsbError::EndpointOverflow)?,
#[test]
fn usb_and_two_uarts_have_independent_sequences_queues_and_completion() {
use kaptan_data_channel::{DataChannel, DataChannelRates, TelemetrySamples};
use kaptan_time::Timestamp;
with_link(|host, usb| {
host.configure(usb, true);
host.dtr(usb, true);
let rates = DataChannelRates {
imu_hz: 20,
heartbeat_hz: 2,
range_hz: None,
optical_flow_hz: None,
};
if index >= sizes.len() || sizes[index] != 0 {
return Err(UsbError::InvalidEndpoint);
}
sizes[index] = size;
if kind == EndpointType::Bulk {
match direction {
UsbDirection::In => state.bulk_in = index,
UsbDirection::Out => state.bulk_out = index,
let mut channels = core::array::from_fn::<_, 3, _>(|_| {
DataChannel::<100>::new(1, 1, rates, 100_000).unwrap()
});
let mut uart1 = IrqTransmit::new();
let mut uart2 = IrqTransmit::new();
let mut slow = MockUart::default();
let mut fast = MockUart::default();
let samples = TelemetrySamples::default();
let mut usb_bytes = Vec::new();
let mut expected = Vec::new();
let mut dropped = false;
for tick in 0..400 {
let now = Timestamp::from_microseconds(tick * 10_000);
let frames = channels
.each_mut()
.map(|channel| channel.on_service_tick(&samples, now));
if let [Some(a), Some(b), Some(c)] = frames {
assert_eq!(a.frame.raw_bytes(), b.frame.raw_bytes());
assert_eq!(b.frame.raw_bytes(), c.frame.raw_bytes());
expected.extend_from_slice(c.frame.raw_bytes());
assert!(usb.send(a));
dropped |= !uart1.send(&mut slow, b);
assert!(uart2.send(&mut fast, c));
}
flush_uart(&mut uart2, &mut fast);
usb.poll();
usb_bytes.extend(host.collect_packets(usb).into_iter().flatten());
}
Ok(EndpointAddress::from_parts(index, direction))
}
fn enable(&mut self) {}
fn reset(&self) {
let mut state = self.0.lock().unwrap();
state.input.fill(None);
state.output.fill(None);
state.events.clear();
}
fn set_device_address(&self, _: u8) {}
fn write(&self, address: EndpointAddress, bytes: &[u8]) -> Result<usize, UsbError> {
let mut state = self.0.lock().unwrap();
if bytes.len() > usize::from(state.sizes[1][address.index()]) {
return Err(UsbError::BufferOverflow);
}
let packet = &mut state.input[address.index()];
if packet.is_some() {
return Err(UsbError::WouldBlock);
}
*packet = Some(bytes.to_vec());
Ok(bytes.len())
}
fn read(&self, address: EndpointAddress, bytes: &mut [u8]) -> Result<usize, UsbError> {
let mut state = self.0.lock().unwrap();
let packet = state.output[address.index()]
.as_ref()
.ok_or(UsbError::WouldBlock)?;
if packet.len() > bytes.len() {
return Err(UsbError::BufferOverflow);
}
let packet = state.output[address.index()].take().unwrap();
bytes[..packet.len()].copy_from_slice(&packet);
Ok(packet.len())
}
fn set_stalled(&self, address: EndpointAddress, stalled: bool) {
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()] = stalled;
}
fn is_stalled(&self, address: EndpointAddress) -> bool {
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()]
}
fn suspend(&self) {}
fn resume(&self) {}
fn poll(&self) -> PollResult {
self.0
.lock()
.unwrap()
.events
.pop_front()
.unwrap_or(PollResult::None)
}
}
impl Host {
fn event(&self, event: PollResult) {
self.0.lock().unwrap().events.push_back(event);
}
fn control(&self, link: &mut UsbMavlink<'_, PacketBus>, request: [u8; 8]) {
self.0.lock().unwrap().output[0] = Some(request.to_vec());
self.event(PollResult::Data {
ep_out: 0,
ep_in_complete: 0,
ep_setup: 1,
});
link.poll();
assert_eq!(self.take_input(0), Some(Vec::new()));
link.poll();
}
fn configure(&self, link: &mut UsbMavlink<'_, PacketBus>, configured: bool) {
self.control(link, [0, 9, u8::from(configured), 0, 0, 0, 0, 0]);
}
fn dtr(&self, link: &mut UsbMavlink<'_, PacketBus>, open: bool) {
self.control(link, [0x21, 0x22, u8::from(open), 0, 0, 0, 0, 0]);
}
fn write_packet(&self, bytes: &[u8]) {
assert!(bytes.len() <= 64);
let mut state = self.0.lock().unwrap();
let endpoint = state.bulk_out;
assert!(state.output[endpoint].is_none());
state.output[endpoint] = Some(bytes.to_vec());
state.events.push_back(PollResult::Data {
ep_out: 1 << endpoint,
ep_in_complete: 0,
ep_setup: 0,
});
}
fn take_input(&self, endpoint: usize) -> Option<Vec<u8>> {
let mut state = self.0.lock().unwrap();
let packet = state.input[endpoint].take()?;
state.events.push_back(PollResult::Data {
ep_out: 0,
ep_in_complete: 1 << endpoint,
ep_setup: 0,
});
Some(packet)
}
fn collect_packets(&self, link: &mut UsbMavlink<'_, PacketBus>) -> Vec<Vec<u8>> {
let endpoint = self.0.lock().unwrap().bulk_in;
let mut packets = Vec::new();
for _ in 0..16 {
let Some(packet) = self.take_input(endpoint) else {
return packets;
};
packets.push(packet);
link.poll();
}
panic!("transmission did not finish");
}
}
fn with_link(test: impl FnOnce(&Host, &mut UsbMavlink<'_, PacketBus>)) {
let host = Host::default();
let allocator = UsbBusAllocator::new(PacketBus(host.0.clone()));
let class = CdcAcmClass::new(&allocator, 64);
let device = UsbDeviceBuilder::new(&allocator, UsbVidPid(0x1209, 1))
.max_packet_size_0(64)
.unwrap()
.build();
let mut link = UsbMavlink::new(device, class);
host.event(PollResult::Reset);
link.poll();
host.configure(&mut link, true);
host.dtr(&mut link, true);
assert!(link.is_open());
test(&host, &mut link);
assert!(
dropped,
"blocked UART must exhaust only its own bounded queue"
);
assert!(slow.bytes.is_empty());
assert_eq!(usb_bytes, expected);
assert_eq!(fast.bytes, expected);
let fast_before = fast.bytes.clone();
flush_uart(&mut uart1, &mut slow);
assert!(!slow.bytes.is_empty());
assert_eq!(
fast.bytes, fast_before,
"UART1 completions cannot advance UART2"
);
});
}
fn pending(frame: MavlinkFrame) -> PendingFrame<MavlinkFrame> {
@@ -269,7 +205,7 @@ fn drains_endpoint_backlog_after_its_poll_event_was_consumed() {
assert_eq!(link.take_received().unwrap().raw(), &first);
assert!(link.take_received().is_none());
assert!(host.0.lock().unwrap().events.is_empty());
assert!(host.events_empty());
assert!(link.poll());
assert_eq!(link.take_received().unwrap().raw(), &second);
});
@@ -432,3 +368,30 @@ fn bus_reset_discards_transmit_and_receive_state() {
assert_eq!(link.take_received().unwrap().raw(), &next);
});
}
#[test]
fn bus_fault_closes_link_and_discards_pending_data() {
for fail in [
usb_bus::Host::fail_poll,
usb_bus::Host::fail_read,
usb_bus::Host::fail_write,
] {
with_link(|host, link| {
let frame = file_frame(130);
assert!(link.send(pending(frame)));
host.write_packet(&frame.raw_bytes()[..8]);
link.poll();
assert!(link.take_received().is_none());
fail(host);
link.with_rx_interrupt_gate(
|link| assert!(!link.poll()),
|_| panic!("interrupt register access after terminal fault"),
);
assert!(!link.is_open());
assert!(!link.send(pending(frame)));
assert!(link.take_received().is_none());
assert!(!link.poll());
assert!(!host.nvic_enabled());
});
}
}