Files
kaptan/firmware/platforms/stm32f7/tests/config_active_state.rs
T

234 lines
6.8 KiB
Rust

#![allow(dead_code)]
use kaptan_config::{Config, ConfigManager, ConfigModel, ConfigStore, ConfigValue, FieldId};
use platform_stm32f7::{
F7ImmutableConfig, F722Hardware, F722ImmutableConfig, ImuMount, LevelFilter, Mavlink, Mpu6000,
Mpu6000Config, Pa4, Pb0Pb1Pb4Pb5Low, Pd2ActiveLow, VerticalSensor,
};
// Include the implementation as a child module so this platform-internal test
// can exercise the crate-private user-candidate adapter without flash hardware.
#[path = "../src/f722/config.rs"]
mod target_config;
use target_config::UserConfigModel;
#[derive(Clone, kaptan_config::Config)]
struct MavlinkConfig {
#[config(id = 101, key = "mavlink.system_id", unit = "id", range(min = 1))]
system_id: u8,
#[config(id = 102, key = "mavlink.component_id", unit = "id", range(min = 1))]
component_id: u8,
#[config(
enum,
id = 105,
key = "mavlink.logging.level_filter",
unit = "log_level"
)]
log_level_filter: LevelFilter,
}
#[derive(Clone, kaptan_config::Config)]
struct TelemetryConfig {
#[config(
id = 103,
key = "telemetry.raw_imu_hz",
unit = "Hz",
range(min = 1, max = 100)
)]
raw_imu_hz: u8,
#[config(
id = 104,
key = "telemetry.heartbeat_hz",
unit = "Hz",
range(min = 1, max = 10)
)]
heartbeat_hz: u8,
}
#[derive(Clone, kaptan_config::Config)]
struct DriverConfig {
#[config(
id = 1000,
key = "test_driver.gain",
unit = "count",
range(min = 1, max = 100)
)]
driver_gain: u8,
}
#[derive(Clone, kaptan_config::Config)]
struct TestUserConfig {
mavlink: MavlinkConfig,
telemetry: TelemetryConfig,
driver: DriverConfig,
}
const IMMUTABLE: F722ImmutableConfig = F722ImmutableConfig {
name: "active-candidate-test",
service_hz: 100,
hardware: F722Hardware::new()
.output_inhibit(Pb0Pb1Pb4Pb5Low)
.imu(
Mpu6000::on_spi1()
.with_drdy(Pa4)
.mounted(ImuMount::BodyAligned)
.configured(Mpu6000Config {
accelerometer: platform_stm32f7::AccelerometerRange::PlusMinus4G,
gyroscope: platform_stm32f7::GyroscopeRange::PlusMinus500DegreesPerSecond,
digital_low_pass_filter: platform_stm32f7::DigitalLowPassFilter::Bandwidth42Hz,
sample_rate_divider: 9,
}),
)
.mavlink(Mavlink::on_usart1().baud(115_200))
.vertical_sensor(VerticalSensor::none())
.status_led(Pd2ActiveLow)
.telemetry_only(),
};
struct OneRecord<C>(Option<C>);
impl<C: Clone> ConfigStore<C> for OneRecord<C> {
type Error = ();
fn load(&mut self) -> Result<Option<C>, Self::Error> {
Ok(self.0.take())
}
fn persist(&mut self, candidate: &C) -> Result<(), Self::Error> {
self.0 = Some(candidate.clone());
Ok(())
}
}
fn factory() -> TestUserConfig {
TestUserConfig {
mavlink: MavlinkConfig {
system_id: 1,
component_id: 1,
log_level_filter: LevelFilter::Info,
},
telemetry: TelemetryConfig {
raw_imu_hz: 20,
heartbeat_hz: 2,
},
driver: DriverConfig { driver_gain: 7 },
}
}
struct MalformedProjection {
inner: TestUserConfig,
system_id: Option<ConfigValue>,
}
impl Config for MalformedProjection {
const FIELD_COUNT: usize = TestUserConfig::FIELD_COUNT;
const SCHEMA_ID: [u8; 16] = TestUserConfig::SCHEMA_ID;
fn descriptor(index: usize) -> Option<&'static kaptan_config::FieldDescriptor> {
TestUserConfig::descriptor(index)
}
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
if id == FieldId::new(101) {
self.system_id
} else {
self.inner.field_value(id)
}
}
fn set_field(
&mut self,
id: FieldId,
value: ConfigValue,
) -> Result<(), kaptan_config::FieldWriteError> {
self.inner.set_field(id, value)
}
fn validate(&self) -> Result<(), kaptan_config::ConfigViolation> {
self.inner.validate()
}
}
#[test]
fn mavlink_logging_policy_is_projected_and_validated_independently() {
assert_eq!(
core::mem::size_of::<target_config::F722UserRuntime>(),
9,
"runtime projection must retain narrow board-native scalar widths"
);
let mut candidate = UserConfigModel::<TestUserConfig, F722ImmutableConfig>::new(factory());
candidate
.set_field(FieldId::new(105), ConfigValue::U32(0))
.unwrap();
let active = candidate.validate(&IMMUTABLE).unwrap();
let runtime = target_config::project_runtime(&active, &IMMUTABLE).unwrap();
assert_eq!(runtime.mavlink_log_level_filter, LevelFilter::Off);
}
#[test]
fn runtime_projection_rejects_missing_wrong_kind_and_wide_semantic_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(),
system_id,
};
let issue = target_config::project_runtime(&malformed, &IMMUTABLE).unwrap_err();
assert_eq!(issue.primary(), Some(FieldId::new(101)));
assert_eq!(issue.rule(), rule);
}
}
#[test]
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))
.expect("driver field must accept a valid persisted value");
let manager = ConfigManager::recover_compatible_or_factory(
factory,
IMMUTABLE,
OneRecord(Some(persisted)),
)
.expect("synthetic persisted user configuration must recover");
assert_eq!(
manager.active().field_value(FieldId::new(1000)),
Some(ConfigValue::U32(42)),
"runtime construction must receive the validated persisted field"
);
}
#[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))
.unwrap();
manager.commit().unwrap();
assert_eq!(
manager.active().field_value(FieldId::new(1000)),
Some(ConfigValue::U32(7))
);
assert_eq!(
manager.committed_value(FieldId::new(1000)),
Some(ConfigValue::U32(42))
);
}