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

+153 -55
View File
@@ -12,22 +12,72 @@ F722 platform / RTIC application ──► data channel ──► MAVLink teleme
├── physical sensor drivers
└── startup motor-pin inhibition
portable libraries
math ─► navigation ─► control ─► safety
Portable flight libraries (not connected to physical actuators)
FlightEndpoint ──► Navigation / control / phases / safety
│
└── caller-owned synchronous output sink
```
Each F722 board contract pairs a board-owned `UserConfig` schema and explicit
`FACTORY_USER_CONFIG` with a board-owned `ImmutableConfig`. The immutable root
is one literal typestate manifest and the sole source of output inhibition,
physical routes, driver setup, sensor mount, bootstrap line rates, status LED,
image role, and service frequency. The user root contains only settings
consumed by that compiled driver graph.
Each F722 board combines `F722UserConfig` with an immutable hardware manifest.
The manifest selects the IMU route, driver settings, mount, serial inventory,
status LED, optional I²C sensor, motor pins and service frequency. User settings
select UART roles, baud and MAVLink settings. Motor pins are inhibited at startup.
The data channel owns rate selection, mailbox consumption, message encoding,
and bounded TX classification. Board code supplies physical routes, target
schema, and factory values; it does not receive generic output authority.
`F722ImmutableConfig<R>` retains the IMU route type through initialization and
interrupt acknowledgement. The sealed `Mpu6000Spi1` and `Icm42688Spi3` routes
select IMU wiring and backend independently of serial inventory and LED wiring.
Configuration and runtime observability are separate planes:
Each board declares one `PortCatalog<N>::DESCRIPTORS` array. Both
`Ports<Catalog, N>` and `SerialInventory::new(&Catalog::DESCRIPTORS)` use it for
stable UART/field identities, supported roles, baud allowlists and resource
claims. Validation produces a boot-only `PortPlan<N>`. The same const `N` sizes
I/O and protocol storage: five UARTs on each current board. Absent UARTs
have no slots. A declared UART set to Disabled retains its slot but initializes
no peripheral or pins. USB is fixed normal MAVLink outside that inventory,
separate from ROM DFU.
Each MAVLink `Link` has its own scheduler, encoder and receive framing, with a
separate transport queue. A shared collector snapshots sensor mailboxes for all
links. See [MAVLink](mavlink.md) for scheduling and transport behavior.
`kaptan-flight-runtime::FlightEndpoint` runs Quad-X control and
`kaptan-navigation::Navigation` without allocation. Callers supply
observations, independently captured receipt/processing time, clock ticks and a
synchronous output sink. Transport and wire encoding stay outside the core.
The board images do not yet construct a flight endpoint.
See [Safety boundary](safety-boundary.md#shared-runtime-commit-contract) for
epochs, request sequencing and the output commit contract.
`FlightEndpoint::supervise` checks actual time, cached health and phase/stream
deadlines without advancing navigation, integrating control or consuming a
control release. It can inhibit and publish outcomes, but cannot renew enabled
output. Callers must service the endpoint with actual time even when input
stops. The core owns control release and deadline decisions.
Within `FlightEndpoint`, fusion, current prediction and initialization completion
run only when the 100-Hz control service is due. Early `tick` calls supervise
cached evidence without advancing navigation. Ingress admission and its
diagnostics remain immediate, but receiving or queuing fresh observations alone
does not renew capabilities. The actual control gap is checked before a due
navigation update can refresh evidence.
`Navigation` initializes from a stationary window of IMU, fixed-reference local
position/velocity, magnetic, pressure and range observations. It derives attitude
and a fixed launch origin from sensors, with explicit calibration/reference
identities. Its shared 16-state error covariance covers position, velocity,
attitude error, gyro bias, accelerometer bias and barometric datum. Bounded
delayed fusion and current prediction preserve observation timing and capability
evidence.
The navigation `sensor-snapshots` feature enables driver snapshot conversion.
The same flight estimator is available on host and embedded targets.
Navigation owns the 25-m/s² free-flight acceleration bound checked by
complete-profile validation. See
[Configuration](configuration.md#flight-configuration) for required installation
data and the supported vehicle model.
Configuration and diagnostics use separate paths:
```text
FILE_CONTROL / FILE_DATA
@@ -42,50 +92,97 @@ kaptan-file-mavlink ──► virtual FileProvider
kaptan-log / kaptan-err-reportable
│
▼
kaptan-diagnostics-mavlink ──► platform-owned encoder / DMA TX
kaptan-diagnostics-mavlink ──► per-link encoder / bounded TX
```
The file dialect is intentionally Kaptan-native rather than standard MAVLink
FTP, `PARAM_*`, or a QGC adapter. Its two messages know only bounded paths,
bytes, positions, checksums, transfer ownership, and retries. The configuration
provider maps `/config/user.kcfg` to a canonical self-describing artifact.
KCFG carries generated metadata and a complete committed value set; the target
streams it without retaining a whole-file buffer. Saving imports and validates
one complete replacement candidate. The core distinguishes only immutable
boot-active values from the latest committed values. A save cannot mutate
controller or estimator state in flight.
The Kaptan-native file dialect owns bounded transfers and retries across links.
Its provider maps saved, active and factory-reset endpoints to streaming KCFG v3
and the generic configuration lifecycle.
`ConfigManager` owns boot-active values, known committed values or unknown durable
state, and the factory candidate. F722 supplies its UART-disabled setup fallback
through `recover_with`. Readable invalid settings remain inspectable for USB
repair. Saving or factory reset never changes active consumers until reboot.
Target-owned stores use private persistence formats. F722 alternates compact
`KCSP` snapshots between two erase sectors, retaining only the old and
replacement records across the commit boundary. That format is not exposed as the public KCFG file.
Motor allocation/mixer topology is immutable compiled vehicle capability data,
never a scalar field or persistence member. F722 recovers operational settings
before constructing consumers. Its local USB CDC-ACM and directly wired UART
accept only exact-target, CRC-valid Kaptan file requests while physical outputs
remain inhibited. CRC is integrity-only; a shared or remote GCS still needs
production authentication, source binding, and persistent anti-replay state.
Disarmed activation and flight-consumer reconstruction remain separate reviewed
work.
`KCSP` snapshots between two erase sectors, retaining only the old and replacement
records across the commit boundary. That format is not exposed as KCFG. Motor
allocation/mixer topology remains compiled vehicle capability data.
The MAVLink dialect root directly includes one-message XML leaves under
`publish/` and `transfer/`. The `exchange/` contract remains empty until a
current consumer justifies a message. There are no category index XML files or
separate enum catalogs. The build watches the definitions tree and generates
one `generated::kaptan` module; leaves are never separately generated Rust
modules. `kaptan-log` itself has
no MAVLink, platform, time, allocation, or filesystem dependency; its sole
configuration coupling is compile-time enum metadata for `LevelFilter`.
`kaptan-err-reportable` independently owns explicitly assigned reportable-error
identity and a bounded nonblocking ingress; it has no logging, MAVLink,
platform, filesystem, or clock dependency. Event creation is independent from
sink filtering, allowing a future blackbox and MAVLink to select different
policies. The platform drains reportable errors before its bounded critical and
best-effort log queues and removes a pending record only after TX admission.
`LOG` carries one conventional level plus one bounded text field; `ERROR`
carries an explicitly assigned 16-bit ID and the bounded `file:line` of the
reporting point. Its message kind already identifies a reportable firmware
error. Kaptan-native heartbeat and sensor publications remain separate from
diagnostics.
## I/O ownership
Sensor handoff uses owned `LatestMailbox<T>` storage split into exactly one
non-cloneable producer and one non-cloneable consumer. Publishing replaces an
unread value, and taking removes it at most once. Each operation copies or takes
an `Option<T>` inside the platform critical section. No callback, allocation,
retry or wait occurs inside that section. Storage stays borrowed while endpoints
are in use, and moving, dropping or forgetting an endpoint holds no lock. The
backend must exclude every participating context, using Cortex-M single-core exclusion on
F722 and the `critical-section/std` backend for host threads.
UART TX and RX are interrupt-driven, without UART DMA. Each I/O owner retains
the peripheral, TX queue and a 512-byte RX buffer with a typed `Timestamp` per
byte captured at IRQ receipt. The worker takes at most 16 bytes per UART per
turn under a short lock. Protocol parsing, scheduling and persistence run
outside UART locks. Overflow or a hardware RX fault discards the damaged backlog
and resets the decoder before later bytes. These bounds do not establish a
measured latency guarantee.
`kaptan-tx::TransmitQueue` owns bounded admission and one logically active frame.
Pending admission cannot replace that active frame. The queue itself is movable
and does not promise DMA address stability. UART byte writes and USB endpoint
writes borrow or copy its bytes synchronously. MAVLink owns encoding, while
platform I/O owners handle transmission.
USB uses the repository-patched Synopsys driver. Runtime register waits allow
higher-priority interrupts and stop after 5 ms or 1,080,000 polls per wait. These
are fault limits, not measured execution times. A fault holds USB in peripheral
reset until restart and discards its queued data. Brief critical sections protect
USB ownership and endpoint buffers. Boot initialization still runs under RTIC's
startup interrupt mask.
SPI1 and SPI3 DMA each use a once-claimed static pair of 32-byte TX/RX buffers.
`start_transfer` copies in, so hardware retains no caller slice. Completion
copies out only after checking stream disable, SPI quiescence, transfer counts
and faults. Failure to establish shutdown or drain RX quarantines the storage
and prevents reuse. Moving, dropping or forgetting the owner never releases
that claim. The F722 implementation relies on DMA-accessible SRAM, disabled
D-cache and device barriers. Physical SPI shutdown, timing and fault behavior
still require hardware validation.
## Monotonic time
TIM2 is the canonical F722 clock. The configured 108-MHz timer input and PSC=107
produce a free-running 32-bit counter at 1 MHz with ARR=`u32::MAX`. All readers
and the CC1 service IRQ extend one shared `u64` microsecond epoch inside the same
critical section. A consumer may remain silent across wraps if other readers
or IRQ service keep extending that epoch. Combined observations must be strictly
less than 2^32 microseconds apart, about 71.58 minutes, including interrupt
blackouts and debugger halts. NMI and fault handlers must not access this clock.
CC1 schedules the current 100-Hz service at epoch-aligned 10-ms deadlines.
Delayed service skips missed periods in constant work and emits at most one
notification per IRQ. Rearming uses two bounded attempts before pending a
recovery invocation. Callbacks and notifications run after clock exclusion ends.
TIM2 is not reset for each service tick, and no update IRQ or timer DMA is used.
Its IRQ is enabled only at the successful end of initialization.
Ordinary WFI retains the timer clock. Deep sleep, STOP/Standby, clock-tree changes,
counter reset/regression and debugger halts spanning a full wrap are unsupported
and require platform reset. Uptime must leave one service period before `u64`
microsecond overflow. DWT is used only for the CPU-running SRAM flash-poll timeout
at 216 MHz, not sensor timestamps or service deadlines. These contracts do not
establish measured oscillator accuracy, IRQ latency, jitter or worst-case timing.
## Diagnostics
`kaptan-log` has no MAVLink, platform, time, allocation or filesystem dependency.
Its configuration coupling is compile-time enum metadata for `LevelFilter`.
`kaptan-err-reportable` independently owns explicit error identities and a bounded
nonblocking ingress, without logging, transport or clock dependencies.
Event creation is independent from sink filtering. The worker drains reportable
errors before logs and offers each diagnostic to all MAVLink links. Heartbeat
and sensor publications use separate scheduling.
See [Configuration](configuration.md) for KCFG and the file-transfer contract,
[Logs](logs.md) for the receiver contract, and [Errors](errors.md)
@@ -96,7 +193,8 @@ for derived-error and failure-reporting scope.
- A peripheral owner is local to the platform runtime.
- Portable crates use typed units and validated values rather than raw wire or
timer representations.
- A safety permit is epoch-bound and must be revalidated at the only nonzero
output boundary.
- A failed observation or numerical check does not partially commit estimator,
controller, allocator, or safety state.
- `SafetySupervisor::evaluate_and_commit` checks current evidence at the
synchronous enabled-output boundary. No transferable permit escapes it.
- Rejected numerical updates do not partially commit computational candidates.
Fault latches, inhibition and ingress replay watermarks follow their explicit
failure contracts rather than rolling back authority or replay history.