feat(mavlink): adopt the kaptan-native dialect
This commit is contained in:
1 parent
1c90870634
commit
5f91465e66
87 files changed
+7733
-6850
No files matched your search
@@ -17,7 +17,7 @@ test = false
|
||||
[dependencies]
|
||||
kaptan-err-reportable = { path = "../../libs/kaptan-err-reportable" }
|
||||
kaptan-config = { path = "../../libs/kaptan-config" }
|
||||
kaptan-config-mavlink = { path = "../../libs/kaptan-config-mavlink" }
|
||||
kaptan-file-mavlink = { path = "../../libs/kaptan-file-mavlink" }
|
||||
kaptan-data-channel = { path = "../../libs/kaptan-data-channel" }
|
||||
kaptan-dma = { path = "../../libs/kaptan-dma" }
|
||||
kaptan-drivers = { path = "../../libs/kaptan-drivers" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -32,11 +32,11 @@ struct MavlinkConfig {
|
||||
struct TelemetryConfig {
|
||||
#[config(
|
||||
id = 103,
|
||||
key = "telemetry.raw_imu_hz",
|
||||
key = "telemetry.imu_hz",
|
||||
unit = "Hz",
|
||||
range(min = 1, max = 100)
|
||||
)]
|
||||
raw_imu_hz: u8,
|
||||
imu_hz: u8,
|
||||
#[config(
|
||||
id = 104,
|
||||
key = "telemetry.heartbeat_hz",
|
||||
@@ -91,7 +91,7 @@ struct OneRecord<C>(Option<C>);
|
||||
impl<C: Clone> ConfigStore<C> for OneRecord<C> {
|
||||
type Error = ();
|
||||
|
||||
fn load(&mut self) -> Result<Option<C>, Self::Error> {
|
||||
fn load(&mut self, _: &C) -> Result<Option<C>, Self::Error> {
|
||||
Ok(self.0.take())
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ fn factory() -> TestUserConfig {
|
||||
log_level_filter: LevelFilter::Info,
|
||||
},
|
||||
telemetry: TelemetryConfig {
|
||||
raw_imu_hz: 20,
|
||||
imu_hz: 20,
|
||||
heartbeat_hz: 2,
|
||||
},
|
||||
driver: DriverConfig { driver_gain: 7 },
|
||||
@@ -123,6 +123,7 @@ struct MalformedProjection {
|
||||
|
||||
impl Config for MalformedProjection {
|
||||
const FIELD_COUNT: usize = TestUserConfig::FIELD_COUNT;
|
||||
const SNAPSHOT_VALUE_BYTES: usize = TestUserConfig::SNAPSHOT_VALUE_BYTES;
|
||||
const SCHEMA_ID: [u8; 16] = TestUserConfig::SCHEMA_ID;
|
||||
|
||||
fn descriptor(index: usize) -> Option<&'static kaptan_config::FieldDescriptor> {
|
||||
@@ -159,7 +160,7 @@ fn mavlink_logging_policy_is_projected_and_validated_independently() {
|
||||
);
|
||||
let mut candidate = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
|
||||
candidate
|
||||
.set_field(FieldId::new(105), ConfigValue::U32(0))
|
||||
.set_field(FieldId::new(105), ConfigValue::U8(0))
|
||||
.unwrap();
|
||||
|
||||
let active = candidate.validate(&IMMUTABLE).unwrap();
|
||||
@@ -168,17 +169,13 @@ fn mavlink_logging_policy_is_projected_and_validated_independently() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_projection_rejects_missing_wrong_kind_and_wide_semantic_values() {
|
||||
fn runtime_projection_rejects_missing_and_wrong_kind_values() {
|
||||
for (system_id, rule) in [
|
||||
(None, kaptan_config::ValidationRule::Inconsistent),
|
||||
(
|
||||
Some(ConfigValue::Bool(true)),
|
||||
kaptan_config::ValidationRule::Inconsistent,
|
||||
),
|
||||
(
|
||||
Some(ConfigValue::U32(256)),
|
||||
kaptan_config::ValidationRule::AboveMaximum,
|
||||
),
|
||||
] {
|
||||
let malformed = MalformedProjection {
|
||||
inner: factory(),
|
||||
@@ -195,7 +192,7 @@ fn runtime_construction_reads_the_persisted_board_owned_user_root() {
|
||||
let factory = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
|
||||
let mut persisted = factory.clone();
|
||||
persisted
|
||||
.set_field(FieldId::new(1000), ConfigValue::U32(42))
|
||||
.set_field(FieldId::new(1000), ConfigValue::U8(42))
|
||||
.expect("driver field must accept a valid persisted value");
|
||||
|
||||
let manager = ConfigManager::recover_compatible_or_factory(
|
||||
@@ -207,7 +204,7 @@ fn runtime_construction_reads_the_persisted_board_owned_user_root() {
|
||||
|
||||
assert_eq!(
|
||||
manager.active().field_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U32(42)),
|
||||
Some(ConfigValue::U8(42)),
|
||||
"runtime construction must receive the validated persisted field"
|
||||
);
|
||||
}
|
||||
@@ -215,19 +212,19 @@ fn runtime_construction_reads_the_persisted_board_owned_user_root() {
|
||||
#[test]
|
||||
fn committing_a_user_edit_does_not_mutate_the_boot_active_view() {
|
||||
let factory = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
|
||||
let mut manager = ConfigManager::new(factory, IMMUTABLE, OneRecord(None)).unwrap();
|
||||
manager.begin().unwrap();
|
||||
manager
|
||||
.set(FieldId::new(1000), ConfigValue::U32(42))
|
||||
let mut manager = ConfigManager::recover(factory, IMMUTABLE, OneRecord(None)).unwrap();
|
||||
let mut replacement = manager.committed().clone();
|
||||
replacement
|
||||
.set_field(FieldId::new(1000), ConfigValue::U8(42))
|
||||
.unwrap();
|
||||
manager.commit().unwrap();
|
||||
manager.replace(replacement).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manager.active().field_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U32(7))
|
||||
Some(ConfigValue::U8(7))
|
||||
);
|
||||
assert_eq!(
|
||||
manager.committed_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U32(42))
|
||||
manager.committed().field_value(FieldId::new(1000)),
|
||||
Some(ConfigValue::U8(42))
|
||||
);
|
||||
}
|
||||
@@ -141,13 +141,17 @@ fn ordinary_f722_image_has_direct_configuration_after_output_inhibition() {
|
||||
"ordinary board images must not hide configuration behind a development feature"
|
||||
);
|
||||
for required in [
|
||||
"configuration: F722ConfigManager",
|
||||
"config_protocol: ConfigProtocol",
|
||||
"receiver: IntegrityMavlinkReceiver<2>",
|
||||
"config_lease_ticks: u16",
|
||||
"wire::decode_command",
|
||||
"self.config_protocol.dispatch_from_composition",
|
||||
"visit_response(response, &mut sender)",
|
||||
"config_file: F722ConfigFile",
|
||||
"file_service: FileService",
|
||||
"receiver: IntegrityMavlinkReceiver<1>",
|
||||
"file_lease_ticks: u16",
|
||||
"TRANSFER_REPLAY_LEASE_SECONDS",
|
||||
"let Some(message) = frame.decode()",
|
||||
"wire::decode_request",
|
||||
".dispatch(&mut self.config_file",
|
||||
"send_file_response",
|
||||
"let class = kaptan_mavlink::transmitter::FrameClass::Critical",
|
||||
"control.status != FileStatus::RequestConflict",
|
||||
] {
|
||||
assert!(
|
||||
board.contains(required),
|
||||
@@ -163,23 +167,15 @@ fn ordinary_f722_image_has_direct_configuration_after_output_inhibition() {
|
||||
"the MPU runtime may drive only PB2 atomically, not retain GPIOB ownership"
|
||||
);
|
||||
assert!(!board.contains("development-config"));
|
||||
assert!(
|
||||
!board.contains("ArmRequest"),
|
||||
"telemetry must not decode arm commands"
|
||||
);
|
||||
assert!(
|
||||
!board.contains("MAV_CMD_COMPONENT_ARM_DISARM") && !board.contains("decode_arm_request"),
|
||||
"telemetry must not accept an arm command"
|
||||
);
|
||||
assert!(
|
||||
!firmware.contains("shared = [mavlink, motors]"),
|
||||
"the MAVLink worker must not share a motor resource"
|
||||
);
|
||||
assert!(
|
||||
firmware.contains("const MAVLINK_EVENT_RECEIVED")
|
||||
&& firmware.contains("mavlink.receive_config_interrupt(event)")
|
||||
&& firmware.contains("mavlink.receive_mavlink_interrupt(event)")
|
||||
&& !firmware.contains("development-config"),
|
||||
"ordinary images must always dispatch received configuration frames"
|
||||
"ordinary images must always dispatch received MAVLink frames"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -279,7 +275,7 @@ fn configuration_schema_is_generated_without_board_forwarding_code() {
|
||||
for semantic_id in [
|
||||
"MAVLINK_SYSTEM_ID",
|
||||
"MAVLINK_COMPONENT_ID",
|
||||
"RAW_IMU_HZ",
|
||||
"IMU_HZ",
|
||||
"HEARTBEAT_HZ",
|
||||
"MAVLINK_LOG_LEVEL_FILTER",
|
||||
"RANGE_HZ",
|
||||
@@ -330,8 +326,8 @@ fn f722_link_budget_reserves_only_deterministic_telemetry() {
|
||||
"ERROR_RECORDS_PER_SECOND",
|
||||
"LOG_RECORDS_PER_SECOND",
|
||||
"WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND",
|
||||
"KAPTAN_ERROR_FRAME_MAXIMUM_BYTES",
|
||||
"KAPTAN_LOG_FRAME_MAXIMUM_BYTES",
|
||||
"ERROR_FRAME_MAXIMUM_BYTES",
|
||||
"LOG_FRAME_MAXIMUM_BYTES",
|
||||
"budget_ticks",
|
||||
"error_budget",
|
||||
"log_budget",
|
||||
@@ -385,6 +381,7 @@ fn mavlink_transport_faults_do_not_recurse_or_retain_dead_accounting() {
|
||||
#[test]
|
||||
fn f722_producers_keep_logs_and_reportable_errors_separate() {
|
||||
let board = platform_source("src/f722/board.rs");
|
||||
let config = platform_source("src/f722/config.rs");
|
||||
let logger = platform_source("../../libs/kaptan-log/src/facade.rs");
|
||||
let errors = platform_source("../../libs/kaptan-err-reportable/src/ingress.rs");
|
||||
|
||||
@@ -400,6 +397,14 @@ fn f722_producers_keep_logs_and_reportable_errors_separate() {
|
||||
);
|
||||
}
|
||||
assert!(board.contains("#[kaptan_err_reportable::propagate]"));
|
||||
assert!(
|
||||
config.contains("enum PersistConfigurationError {")
|
||||
&& config.contains("#[kaptan_error(id = 2210)]")
|
||||
&& config.contains("#[kaptan_error(id = 2209)]")
|
||||
&& config.contains("impl From<JournalError<F722FlashError>>")
|
||||
&& config.contains("fn persist_candidate(")
|
||||
&& config.contains("#[kaptan_err_reportable::propagate]")
|
||||
);
|
||||
assert!(!board.contains("report_error"));
|
||||
assert!(!board.contains("report_static_error"));
|
||||
assert!(logger.contains("GLOBAL_INGRESS_CAPACITY"));
|
||||
|
||||
Reference in new issue
Block a user