feat(mavlink): adopt the kaptan-native dialect

This commit is contained in:
Onur Özkan committed 2026-08-24 00:56:06 +03:00
1 parent 1c90870634
commit 5f91465e66
87 files changed
+7733 -6850

No files matched your search

+71 -121
View File
@@ -27,27 +27,20 @@ use kaptan_log::{DeliveryClass, LogSinkQueue, TransportAdmission};
use kaptan_spi::SpiCompletion;
use stm32f7::stm32f722;
use kaptan_config_mavlink::{
ConfigProtocol, Operation, ResponseVisitor, ResultKind, ServiceResponse, Target,
visit_response, wire,
};
use kaptan_mavlink::{
MavlinkVersion,
message::{Message, kaptan},
receiver::IntegrityMavlinkReceiver,
use kaptan_file_mavlink::{
FileResponse, FileService, FileStatus, Principal, TRANSFER_REPLAY_LEASE_SECONDS, Target, wire,
};
use kaptan_mavlink::{generated::kaptan, receiver::IntegrityMavlinkReceiver};
use crate::{BoardError, F7ImmutableConfig, F722Hardware, F722Profile, F722StatusLedRoute};
use super::config::{
F722ConfigManager, F722ConfigStore, F722Nor, UserConfigModel, project_runtime,
validate_runtime_values,
F722ConfigFile, F722ConfigManager, F722ConfigStore, F722Nor, UserConfigModel, project_runtime,
};
use super::{
VerticalRoute, clock,
data_channel::{
F722MavlinkTransmitter, F722StatusLed, GeprcStatusLed, SERVICE_HZ, SERVICE_TIMER_CONFIG,
vertical_data_config,
},
interrupts,
sensors::{F722Imu, ImuEvent},
@@ -57,8 +50,10 @@ const UART4_CLOCK_HZ: u32 = 54_000_000;
const I2C1_CONFIG: I2c1Config = I2c1Config::from_timing_register(0x2040_4768);
const LIDAR_MODE: AcquisitionMode = AcquisitionMode::WithReceiverBiasCorrection;
const LIDAR_STARTUP_TICKS: u8 = 3;
const CONFIG_SESSION_LEASE_TICKS: u16 = (SERVICE_HZ * 10) as u16;
const SCHEDULED_FRAME_MAXIMUM_BYTES: usize = 48;
const FILE_TRANSFER_LEASE_TICKS: u16 = (SERVICE_HZ * TRANSFER_REPLAY_LEASE_SECONDS as usize) as u16;
// The IMU is the largest current scheduled payload; unsigned MAVLink 2 adds a
// ten-byte header and two-byte checksum.
const SCHEDULED_FRAME_MAXIMUM_BYTES: usize = 12 + kaptan::IMU_DATA::ENCODED_LEN;
// User/immutable validation permits at most one scheduled frame per 100 Hz
// service tick. Configuration replies and diagnostics are event-driven and
// use bounded queue admission; they do not invent a periodic link reservation.
@@ -93,10 +88,10 @@ where
I: F7ImmutableConfig,
{
data_channel: DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>,
configuration: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>,
config_protocol: ConfigProtocol,
receiver: IntegrityMavlinkReceiver<2>,
config_lease_ticks: u16,
config_file: F722ConfigFile<C, I, MAX_PAYLOAD_BYTES>,
file_service: FileService,
receiver: IntegrityMavlinkReceiver<1>,
file_lease_ticks: u16,
mavlink_errors: ErrorSink,
mavlink_logs: LogSinkQueue<1, 1>,
led: F722StatusLed,
@@ -142,11 +137,11 @@ pub(crate) fn validate_board_config<C, I>(
hardware: F722Hardware,
) -> Result<(), BoardError>
where
C: Clone + Config,
C: Config,
I: F7ImmutableConfig,
{
<C as kaptan_config::Config>::validate(config).map_err(|_| BoardError::Configuration)?;
validate_runtime_values(config, immutable).map_err(|_| BoardError::Configuration)?;
let _ = project_runtime(config, immutable).map_err(|_| BoardError::Configuration)?;
let immutable = immutable.f722();
if immutable.service_hz != SERVICE_HZ {
return Err(BoardError::UnsupportedServiceFrequency);
@@ -249,7 +244,6 @@ where
device.TIM2,
config,
immutable,
vertical,
imu,
range,
mtf01,
@@ -317,7 +311,6 @@ where
device.TIM2,
config,
immutable,
vertical,
imu,
range,
mtf01,
@@ -334,7 +327,6 @@ fn finish<C, I, const MAX_PAYLOAD_BYTES: usize>(
tim2: stm32f722::TIM2,
config: C,
immutable: I,
vertical: VerticalRoute,
imu: F722Imu,
range: Option<F722Range>,
mtf01: Option<F722Mtf01>,
@@ -348,12 +340,10 @@ where
I: F7ImmutableConfig,
{
let runtime = project_runtime(&config, &immutable).map_err(|_| BoardError::Configuration)?;
let immutable = immutable.f722();
let system_id = runtime.system_id;
let component_id = runtime.component_id;
let rates = DataChannelRates {
service_hz: immutable.service_hz,
raw_imu_hz: usize::from(runtime.raw_imu_hz),
imu_hz: usize::from(runtime.imu_hz),
range_hz: runtime.range_hz.map(usize::from),
optical_flow_hz: runtime.optical_flow_hz.map(usize::from),
heartbeat_hz: usize::from(runtime.heartbeat_hz),
@@ -364,7 +354,7 @@ where
system_id,
component_id,
rates,
vertical_data_config(vertical),
10,
DataSources {
imu: &SAMPLES,
range: &RANGES,
@@ -383,10 +373,10 @@ where
},
mavlink: F722MavlinkState {
data_channel,
configuration,
config_protocol: ConfigProtocol::new(Target::new(system_id, component_id)),
config_file: F722ConfigFile::new(configuration),
file_service: FileService::new(Target::new(system_id, component_id)),
receiver: IntegrityMavlinkReceiver::new(),
config_lease_ticks: 0,
file_lease_ticks: 0,
mavlink_errors: ErrorSink::new(),
mavlink_logs: LogSinkQueue::new(mavlink_log_filter),
led,
@@ -404,8 +394,7 @@ where
I: F7ImmutableConfig,
{
let candidate = UserConfigModel::<C, I>::new(factory);
let store =
F722ConfigStore::<C, I, MAX_PAYLOAD_BYTES>::new(F722Nor::new(flash), candidate.clone());
let store = F722ConfigStore::<MAX_PAYLOAD_BYTES>::new(F722Nor::new(flash));
F722ConfigManager::<C, I, MAX_PAYLOAD_BYTES>::recover_compatible_or_factory(
candidate, immutable, store,
)
@@ -595,7 +584,7 @@ where
const DMA: u8 = 1 << 2;
if events & RECEIVED != 0 {
self.drain_config_frames();
self.drain_file_frames();
}
if events & DMA != 0 {
@@ -607,77 +596,82 @@ where
}
if events & TIMER != 0 {
self.tick_config_lease();
// The board is initialized and transmitting physical telemetry,
// but has no flight runtime or output authority. `STANDBY` with
// an unarmed base mode communicates that accurately; `UNINIT`
// would incorrectly claim initialization never completed.
self.data_channel
.on_service_tick(kaptan_mavlink::message::MavState::MAV_STATE_STANDBY, false);
self.tick_file_lease();
self.data_channel.on_service_tick();
self.drain_one_mavlink_diagnostic();
}
}
pub fn receive_config_interrupt(&mut self, event: ReceiveInterrupt) -> bool {
pub fn receive_mavlink_interrupt(&mut self, event: ReceiveInterrupt) -> bool {
match event {
ReceiveInterrupt::Byte(byte) => self.receiver.receive_byte(byte),
ReceiveInterrupt::Fault => {
self.receiver.reset();
self.config_lease_ticks = 0;
self.config_protocol.expire(&mut self.configuration);
kaptan_log::warn!("mavlink receive fault; configuration session expired");
self.file_lease_ticks = 0;
self.file_service.expire(&mut self.config_file);
kaptan_log::warn!("mavlink receive fault; file transfer expired");
false
}
ReceiveInterrupt::NotPending => false,
}
}
fn drain_config_frames(&mut self) {
fn drain_file_frames(&mut self) {
while let Some(frame) = self.receiver.take_received() {
if frame.system_id() == 0 || frame.component_id() == 0 {
continue;
}
let Ok(message) =
kaptan::MavMessage::parse(MavlinkVersion::V2, frame.message_id(), frame.payload())
let Some(principal) =
Principal::try_new(frame.raw().system_id(), frame.raw().component_id())
else {
continue;
};
let Ok(command) = wire::decode_command(&message) else {
let Some(message) = frame.decode() else {
continue;
};
let principal =
kaptan_config_mavlink::Principal::new(frame.system_id(), frame.component_id());
let mut sender = ConfigResponseSender {
data_channel: &mut self.data_channel,
let Ok(request) = wire::decode_request(&message) else {
continue;
};
let mut refresh_lease = false;
let _: Result<(), ()> = self.config_protocol.dispatch_from_composition(
&mut self.configuration,
principal,
command,
|response| {
if let ServiceResponse::Result(reply) = response
&& reply.result != ResultKind::Rejected
{
refresh_lease |= reply.operation != Operation::Query;
}
visit_response(response, &mut sender)
},
);
let Some(response) =
self.file_service
.dispatch(&mut self.config_file, principal, request)
else {
continue;
};
let refresh_lease = match response {
FileResponse::Data(_) => true,
FileResponse::Control(control) => {
control.transfer_id != 0
&& control.status != FileStatus::Busy
&& control.status != FileStatus::InvalidTransfer
&& control.status != FileStatus::RequestConflict
}
};
self.send_file_response(response);
if refresh_lease {
self.config_lease_ticks = CONFIG_SESSION_LEASE_TICKS;
self.file_lease_ticks = FILE_TRANSFER_LEASE_TICKS;
}
}
}
fn tick_config_lease(&mut self) {
if self.config_lease_ticks == 0 {
fn tick_file_lease(&mut self) {
if self.file_lease_ticks == 0 {
return;
}
self.config_lease_ticks -= 1;
if self.config_lease_ticks == 0 {
self.config_protocol.expire(&mut self.configuration);
kaptan_log::debug!("configuration session expired");
self.file_lease_ticks -= 1;
if self.file_lease_ticks == 0 && self.file_service.expire(&mut self.config_file) {
kaptan_log::debug!("file transfer expired");
}
}
fn send_file_response(&mut self, response: FileResponse) {
let message = wire::encode_response(response);
let class = kaptan_mavlink::transmitter::FrameClass::Critical;
match message {
kaptan::MavMessage::FILE_CONTROL(data) => {
let _ = self.data_channel.send_message(class, &data);
}
kaptan::MavMessage::FILE_DATA(data) => {
let _ = self.data_channel.send_message(class, &data);
}
_ => unreachable!("file response encoder returned another message"),
}
}
@@ -702,7 +696,7 @@ where
self.mavlink_errors.submit_next(|record| {
let payload = encode_error(record);
data_channel
.send_custom(kaptan_mavlink::transmitter::FrameClass::Critical, &payload)
.send_message(kaptan_mavlink::transmitter::FrameClass::Critical, &payload)
.is_retained()
})
}
@@ -715,7 +709,7 @@ where
DeliveryClass::BestEffort => kaptan_mavlink::transmitter::FrameClass::BestEffort,
};
let payload = encode_log(record);
let admission = data_channel.send_custom(class, &payload);
let admission = data_channel.send_message(class, &payload);
let outcome = if admission.is_retained() {
TransportAdmission::Accepted
} else {
@@ -726,50 +720,6 @@ where
}
}
struct ConfigResponseSender<'a> {
data_channel: &'a mut DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>,
}
impl ResponseVisitor for ConfigResponseSender<'_> {
type Error = ();
fn info(&mut self, response: &kaptan::KAPTAN_CONFIG_INFO_DATA) -> Result<(), Self::Error> {
self.send(response)
}
fn result(&mut self, response: &kaptan::KAPTAN_CONFIG_RESULT_DATA) -> Result<(), Self::Error> {
self.send(response)
}
fn descriptor(
&mut self,
response: &kaptan::KAPTAN_CONFIG_DESCRIPTOR_DATA,
) -> Result<(), Self::Error> {
self.send(response)
}
fn enum_option(
&mut self,
response: &kaptan::KAPTAN_CONFIG_ENUM_OPTION_DATA,
) -> Result<(), Self::Error> {
self.send(response)
}
fn value(&mut self, response: &kaptan::KAPTAN_CONFIG_VALUE_DATA) -> Result<(), Self::Error> {
self.send(response)
}
}
impl ConfigResponseSender<'_> {
fn send<M: kaptan_mavlink::message::MessageData>(&mut self, response: &M) -> Result<(), ()> {
self.data_channel
.send_custom(kaptan_mavlink::transmitter::FrameClass::Critical, response)
.is_retained()
.then_some(())
.ok_or(())
}
}
pub(crate) fn take_mavlink_completion() -> kaptan_dma::TxCompletion {
match UART_COMPLETION.swap(0, Ordering::AcqRel) {
1 => kaptan_dma::TxCompletion::Complete,
+373 -24
View File
@@ -1,9 +1,14 @@
//! Board-owned user configuration support and the generic F722 flash journal.
use core::marker::PhantomData;
use core::{marker::PhantomData, mem};
use kaptan_config::{
Config, ConfigManager, ConfigModel, ConfigValue, ConfigViolation, DualSlotJournal, DualSlotNor,
FieldDescriptor, FieldId, FieldWriteError, ValidationRule,
FieldDescriptor, FieldId, FieldWriteError, JournalError, KcfgDecodeError, KcfgExportPlan,
KcfgExporter, KcfgImporter, KcfgResultDetail, ReplaceError, ReplaceOutcome, ValidationRule,
kcfg_length,
};
use kaptan_file_mavlink::{
CONTENT_BYTES, Content, FileFailure, FileMetadata, FileProvider, FileStatus,
};
use stm32f7::stm32f722;
@@ -11,11 +16,12 @@ use crate::{F7ImmutableConfig, LevelFilter};
const MAVLINK_SYSTEM_ID: FieldId = FieldId::new(101);
const MAVLINK_COMPONENT_ID: FieldId = FieldId::new(102);
const RAW_IMU_HZ: FieldId = FieldId::new(103);
const IMU_HZ: FieldId = FieldId::new(103);
const HEARTBEAT_HZ: FieldId = FieldId::new(104);
const MAVLINK_LOG_LEVEL_FILTER: FieldId = FieldId::new(105);
const RANGE_HZ: FieldId = FieldId::new(106);
const OPTICAL_FLOW_HZ: FieldId = FieldId::new(107);
const USER_CONFIG_PATH: &[u8] = b"/config/user.kcfg";
/// Narrow boot projection consumed only by fixed F722 services.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -23,7 +29,7 @@ pub(super) struct F722UserRuntime {
pub(super) system_id: u8,
pub(super) component_id: u8,
pub(super) mavlink_log_level_filter: LevelFilter,
pub(super) raw_imu_hz: u8,
pub(super) imu_hz: u8,
pub(super) range_hz: Option<u8>,
pub(super) optical_flow_hz: Option<u8>,
pub(super) heartbeat_hz: u8,
@@ -81,22 +87,11 @@ where
fn validate(&self, context: &Self::Context) -> Result<Self::Active, ConfigViolation> {
<C as Config>::validate(&self.candidate)?;
validate_runtime_values(&self.candidate, context)?;
let _ = project_runtime(&self.candidate, context)?;
Ok(self.candidate.clone())
}
}
pub(super) fn validate_runtime_values<C, I>(
candidate: &C,
immutable: &I,
) -> Result<(), ConfigViolation>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
project_runtime(candidate, immutable).map(|_| ())
}
pub(super) fn project_runtime<C, I>(
candidate: &C,
immutable: &I,
@@ -120,7 +115,7 @@ where
ValidationRule::Zero,
));
}
let raw_imu_hz = required_u8(candidate, RAW_IMU_HZ)?;
let imu_hz = required_u8(candidate, IMU_HZ)?;
let heartbeat_hz = required_u8(candidate, HEARTBEAT_HZ)?;
let level_filter = required_u8(candidate, MAVLINK_LOG_LEVEL_FILTER)?;
let mavlink_log_level_filter = LevelFilter::try_from_wire(level_filter).ok_or(
@@ -138,7 +133,7 @@ where
};
let service_hz = immutable.service_hz;
let mut scheduled_hz = checked_rate(raw_imu_hz, RAW_IMU_HZ, service_hz)?
let mut scheduled_hz = checked_rate(imu_hz, IMU_HZ, service_hz)?
.checked_add(checked_rate(heartbeat_hz, HEARTBEAT_HZ, service_hz)?)
.ok_or(ConfigViolation::global(ValidationRule::Inconsistent))?;
scheduled_hz = add_optional_rate(range_hz, RANGE_HZ, service_hz, scheduled_hz)?;
@@ -151,7 +146,7 @@ where
system_id,
component_id,
mavlink_log_level_filter,
raw_imu_hz,
imu_hz,
range_hz,
optical_flow_hz,
heartbeat_hz,
@@ -160,8 +155,7 @@ where
fn required_u8<C: Config>(candidate: &C, field: FieldId) -> Result<u8, ConfigViolation> {
match candidate.field_value(field) {
Some(ConfigValue::U32(value)) => u8::try_from(value)
.map_err(|_| ConfigViolation::field(field, ValidationRule::AboveMaximum)),
Some(ConfigValue::U8(value)) => Ok(value),
Some(_) | None => Err(ConfigViolation::field(field, ValidationRule::Inconsistent)),
}
}
@@ -203,13 +197,368 @@ const FLASH_ERROR_FLAGS: u32 = 0x01f2;
const FLASH_OPERATION_TIMEOUT_CYCLES: u32 = 2 * 216_000_000;
/// One schema-independent journal shared by every STM32F722 board image.
pub(crate) type F722ConfigStore<C, I, const MAX_PAYLOAD_BYTES: usize> =
DualSlotJournal<UserConfigModel<C, I>, F722Nor, MAX_PAYLOAD_BYTES>;
pub(crate) type F722ConfigStore<const MAX_PAYLOAD_BYTES: usize> =
DualSlotJournal<F722Nor, MAX_PAYLOAD_BYTES>;
/// Concrete manager storage selected when the board macro monomorphizes the
/// common platform task graph for its aggregate and exact snapshot size.
pub(crate) type F722ConfigManager<C, I, const MAX_PAYLOAD_BYTES: usize> =
ConfigManager<UserConfigModel<C, I>, F722ConfigStore<C, I, MAX_PAYLOAD_BYTES>>;
ConfigManager<UserConfigModel<C, I>, F722ConfigStore<MAX_PAYLOAD_BYTES>>;
#[derive(Debug, kaptan_err_reportable::KaptanError)]
enum PersistConfigurationError {
/// Reserved journal sectors cannot hold the configured payload bound.
#[kaptan_error(id = 2210)]
JournalGeometry,
/// The compiled configuration model cannot produce a private snapshot.
#[kaptan_error(id = 2202)]
ConfigurationModel,
/// The private journal sequence has exhausted its stable integer domain.
#[kaptan_error(id = 2203)]
JournalSequenceExhausted,
/// A flash request addressed bytes outside the reserved journal sectors.
#[kaptan_error(id = 2204)]
FlashBounds,
/// The embedded flash controller could not be unlocked for programming.
#[kaptan_error(id = 2206)]
FlashUnlock,
/// The embedded flash controller reported a programming or erase fault.
#[kaptan_error(id = 2207)]
FlashController,
/// Flash read-back differed from the word requested by the platform.
#[kaptan_error(id = 2208)]
FlashVerification,
/// A complete journal record failed post-write verification.
#[kaptan_error(id = 2209)]
JournalVerification,
}
impl From<JournalError<F722FlashError>> for PersistConfigurationError {
fn from(error: JournalError<F722FlashError>) -> Self {
match error {
JournalError::InvalidGeometry => Self::JournalGeometry,
JournalError::InvalidModel => Self::ConfigurationModel,
JournalError::SequenceExhausted => Self::JournalSequenceExhausted,
JournalError::Storage(F722FlashError::Bounds) => Self::FlashBounds,
JournalError::Storage(F722FlashError::Unlock) => Self::FlashUnlock,
JournalError::Storage(F722FlashError::Controller) => Self::FlashController,
JournalError::Storage(F722FlashError::Verification) => Self::FlashVerification,
JournalError::Verification => Self::JournalVerification,
}
}
}
enum ConfigFileTransfer<C: ConfigModel> {
Idle,
Read(KcfgExportPlan),
Write(KcfgImporter<C>),
}
/// Streaming KCFG endpoint over the private power-loss-safe flash journal.
///
/// The endpoint retains one typed candidate while writing and generates read
/// chunks directly from compiled metadata. It never buffers the public file.
pub(crate) struct F722ConfigFile<C, I, const MAX_PAYLOAD_BYTES: usize>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
manager: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>,
transfer: ConfigFileTransfer<UserConfigModel<C, I>>,
}
impl<C, I, const MAX_PAYLOAD_BYTES: usize> F722ConfigFile<C, I, MAX_PAYLOAD_BYTES>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
pub(crate) const fn new(manager: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>) -> Self {
Self {
manager,
transfer: ConfigFileTransfer::Idle,
}
}
/// Keeps user-content rejection in the file response while reporting a
/// storage failure once at the persistence boundary.
#[kaptan_err_reportable::propagate]
fn persist_candidate(
&mut self,
candidate: UserConfigModel<C, I>,
) -> Result<Result<ReplaceOutcome, ConfigViolation>, PersistConfigurationError> {
match self.manager.replace(candidate) {
Ok(outcome) => Ok(Ok(outcome)),
Err(ReplaceError::InvalidCandidate(violation)) => Ok(Err(violation)),
Err(ReplaceError::Store(error)) => Err(error.into()),
}
}
}
impl<C, I, const MAX_PAYLOAD_BYTES: usize> FileProvider for F722ConfigFile<C, I, MAX_PAYLOAD_BYTES>
where
C: Clone + Config,
I: F7ImmutableConfig,
{
fn open_read(
&mut self,
path: &kaptan_file_mavlink::FilePath,
) -> Result<FileMetadata, FileFailure> {
if path.as_bytes() != USER_CONFIG_PATH {
return Err(file_failure(FileStatus::NotFound, b"file not found"));
}
if !matches!(self.transfer, ConfigFileTransfer::Idle) {
return Err(file_failure(FileStatus::Busy, b"file transfer busy"));
}
let exporter = KcfgExporter::new(
self.manager.committed(),
self.manager.factory(),
self.manager.restart_required(),
)
.map_err(|_| file_failure(FileStatus::Internal, b"cannot export configuration"))?;
let metadata = FileMetadata {
size: u32::try_from(exporter.length())
.map_err(|_| file_failure(FileStatus::TooLarge, b"configuration file too large"))?,
checksum_crc32c: exporter.checksum_crc32c(),
};
self.transfer = ConfigFileTransfer::Read(exporter.plan());
Ok(metadata)
}
fn open_write(
&mut self,
path: &kaptan_file_mavlink::FilePath,
metadata: FileMetadata,
) -> Result<(), FileFailure> {
if path.as_bytes() != USER_CONFIG_PATH {
return Err(file_failure(FileStatus::NotFound, b"file not found"));
}
if !matches!(self.transfer, ConfigFileTransfer::Idle) {
return Err(file_failure(FileStatus::Busy, b"file transfer busy"));
}
let expected = kcfg_length::<UserConfigModel<C, I>>()
.map_err(|_| file_failure(FileStatus::Internal, b"cannot import configuration"))?;
if usize::try_from(metadata.size) != Ok(expected) {
return Err(kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::failure(),
b"wrong configuration file length",
));
}
let importer = KcfgImporter::new(self.manager.factory())
.map_err(|_| file_failure(FileStatus::Internal, b"cannot import configuration"))?;
self.transfer = ConfigFileTransfer::Write(importer);
Ok(())
}
fn read(&mut self, position: u32, output: &mut [u8]) -> Result<(), FileFailure> {
let ConfigFileTransfer::Read(plan) = &self.transfer else {
return Err(file_failure(
FileStatus::InvalidState,
b"file is not open for read",
));
};
let exporter = KcfgExporter::from_plan_unchecked(
self.manager.committed(),
self.manager.factory(),
*plan,
);
let written = exporter
.read(position as usize, output)
.map_err(|_| file_failure(FileStatus::Internal, b"cannot export configuration"))?;
if written != output.len() {
return Err(file_failure(
FileStatus::Internal,
b"short configuration read",
));
}
Ok(())
}
fn write(&mut self, _: u32, data: &[u8]) -> Result<(), FileFailure> {
let ConfigFileTransfer::Write(importer) = &mut self.transfer else {
return Err(file_failure(
FileStatus::InvalidState,
b"file is not open for write",
));
};
importer
.push(self.manager.factory(), data)
.map_err(kcfg_failure)
}
fn close(&mut self) -> Result<Content, FileFailure> {
match mem::replace(&mut self.transfer, ConfigFileTransfer::Idle) {
ConfigFileTransfer::Idle => {
Err(file_failure(FileStatus::InvalidState, b"file is not open"))
}
ConfigFileTransfer::Read(_) => Ok(Content::EMPTY),
ConfigFileTransfer::Write(importer) => {
let candidate = importer.finish().map_err(kcfg_failure)?;
let outcome = self
.persist_candidate(candidate)
.map_err(|_| {
file_failure(FileStatus::StorageFailure, b"configuration storage failed")
})?
.map_err(|violation| {
kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::violation(violation),
b"configuration validation failed",
)
})?;
Ok(kcfg_content(
KcfgResultDetail::success(outcome.restart_required),
b"",
))
}
}
}
fn abort(&mut self) {
self.transfer = ConfigFileTransfer::Idle;
}
}
fn file_failure(status: FileStatus, detail: &'static [u8]) -> FileFailure {
let content = Content::try_new(detail).expect("static file failure detail is bounded");
FileFailure::new(status, content).expect("failure status must reject the operation")
}
fn kcfg_content(detail: KcfgResultDetail, text: &'static [u8]) -> Content {
let prefix = detail.encode();
let length = prefix.len() + text.len();
assert!(
length <= CONTENT_BYTES,
"KCFG result detail must be bounded"
);
let mut bytes = [0_u8; CONTENT_BYTES];
bytes[..prefix.len()].copy_from_slice(&prefix);
bytes[prefix.len()..length].copy_from_slice(text);
Content::try_new(&bytes[..length]).expect("KCFG result detail is bounded")
}
fn kcfg_file_failure(
status: FileStatus,
detail: KcfgResultDetail,
text: &'static [u8],
) -> FileFailure {
FileFailure::new(status, kcfg_content(detail, text))
.expect("failure status must reject the operation")
}
fn kcfg_failure(error: KcfgDecodeError) -> FileFailure {
let (result, text): (KcfgResultDetail, &'static [u8]) = match error {
KcfgDecodeError::Truncated => {
(KcfgResultDetail::failure(), b"truncated configuration file")
}
KcfgDecodeError::TrailingData => {
(KcfgResultDetail::failure(), b"trailing configuration data")
}
KcfgDecodeError::InvalidMagic => {
(KcfgResultDetail::failure(), b"invalid configuration file")
}
KcfgDecodeError::UnsupportedVersion(_) => (
KcfgResultDetail::failure(),
b"unsupported configuration format",
),
KcfgDecodeError::InvalidArtifactFlags(_) => {
(KcfgResultDetail::failure(), b"invalid configuration flags")
}
KcfgDecodeError::ChecksumMismatch => (
KcfgResultDetail::failure(),
b"configuration checksum mismatch",
),
KcfgDecodeError::InvalidFieldId(_) => (
KcfgResultDetail::failure(),
b"invalid configuration field id",
),
KcfgDecodeError::InvalidValueKind(_) => (
KcfgResultDetail::failure(),
b"invalid configuration value type",
),
KcfgDecodeError::InvalidFieldFlags(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"invalid configuration field flags",
);
}
KcfgDecodeError::InvalidValue(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"invalid configuration value",
);
}
KcfgDecodeError::InvalidText(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"invalid configuration metadata",
);
}
KcfgDecodeError::InvalidEnum(field) | KcfgDecodeError::DuplicateEnumValue(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"invalid configuration options",
);
}
KcfgDecodeError::DuplicateField(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"duplicate configuration field",
);
}
KcfgDecodeError::WrongSchema => (
KcfgResultDetail::failure(),
b"configuration schema mismatch",
),
KcfgDecodeError::WrongFieldCount => (
KcfgResultDetail::failure(),
b"configuration field count mismatch",
),
KcfgDecodeError::DescriptorMismatch(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"configuration metadata mismatch",
);
}
KcfgDecodeError::FactoryMismatch(field) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::field_failure(field),
b"configuration factory mismatch",
);
}
KcfgDecodeError::InvalidModel(_) => {
return kcfg_file_failure(
FileStatus::Internal,
KcfgResultDetail::failure(),
b"invalid firmware configuration schema",
);
}
KcfgDecodeError::InvalidWrite(FieldWriteError::Violation(violation)) => {
return kcfg_file_failure(
FileStatus::ContentRejected,
KcfgResultDetail::violation(violation),
b"configuration value rejected",
);
}
KcfgDecodeError::InvalidWrite(
FieldWriteError::UnknownField(field) | FieldWriteError::WrongType { field, .. },
) => {
return kcfg_file_failure(
FileStatus::Internal,
KcfgResultDetail::field_failure(field),
b"invalid firmware configuration schema",
);
}
};
kcfg_file_failure(FileStatus::ContentRejected, result, text)
}
/// Failure of the STM32F722 embedded-flash controller contract.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -1,11 +1,10 @@
//! F722 transport and vertical-route adapters for the portable data channel.
use super::{VerticalRoute, peripherals};
use kaptan_data_channel::{FrameSink, VerticalDataConfig};
use super::peripherals;
use kaptan_data_channel::FrameSink;
use kaptan_dma::TxCompletion;
use kaptan_mavlink::{
MavlinkFrame,
encoder::DistanceSensorConfig,
transmitter::{EnqueueResult, FrameClass, MavlinkTransmitter},
};
@@ -23,39 +22,6 @@ pub(crate) const SERVICE_TIMER_CONFIG: peripherals::Tim2PeriodicConfig =
auto_reload: 99,
};
/// Maps a selected vertical electrical route to configured data-channel metadata.
///
/// The values are telemetry descriptions only; they neither calibrate a flight
/// estimator nor authorize an `ExternalControlOnly` profile.
pub(crate) fn vertical_data_config(route: VerticalRoute) -> VerticalDataConfig {
let distance =
|minimum_distance_centimetres, maximum_distance_centimetres| DistanceSensorConfig {
sensor_id: 0,
orientation:
kaptan_mavlink::message::MavSensorOrientation::MAV_SENSOR_ROTATION_PITCH_270,
minimum_distance_centimetres,
maximum_distance_centimetres,
};
match route {
VerticalRoute::None => VerticalDataConfig {
distance: None,
optical_flow_sensor_id: None,
maximum_age_ticks: 10,
},
VerticalRoute::Mtf01 => VerticalDataConfig {
distance: Some(distance(2, 800)),
optical_flow_sensor_id: Some(0),
maximum_age_ticks: 10,
},
VerticalRoute::LidarLiteV3 => VerticalDataConfig {
distance: Some(distance(20, 4_000)),
optical_flow_sensor_id: None,
maximum_age_ticks: 10,
},
}
}
/// The two F722 DMA engines behind the portable data-channel transport.
enum F722MavlinkTransport {
Usart1(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart1TxDma, 4>),
@@ -230,7 +230,7 @@ macro_rules! __f722_flight_application {
if context
.shared
.mavlink
.lock(|mavlink| mavlink.receive_config_interrupt(event))
.lock(|mavlink| mavlink.receive_mavlink_interrupt(event))
{
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
}
@@ -242,7 +242,7 @@ macro_rules! __f722_flight_application {
if context
.shared
.mavlink
.lock(|mavlink| mavlink.receive_config_interrupt(event))
.lock(|mavlink| mavlink.receive_mavlink_interrupt(event))
{
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
}
+1 -1
View File
@@ -39,7 +39,7 @@ pub const fn config_snapshot_length<C: Config>() -> usize {
C::FIELD_COUNT <= u16::MAX as usize,
"configuration schema exceeds wire field-count domain"
);
kaptan_config::scalar_snapshot_length(C::FIELD_COUNT as u16)
kaptan_config::scalar_snapshot_length(C::FIELD_COUNT as u16, C::SNAPSHOT_VALUE_BYTES)
}
/// Dependencies named only by the platform's exported RTIC macro expansion.