#![allow(dead_code)] use kaptan_config::{Config, ConfigManager, ConfigModel, ConfigStore, ConfigValue, FieldId}; use platform_stm32f7::{ F7ImmutableConfig, F722Hardware, F722ImmutableConfig, ImuMount, Mavlink, Mpu6000, Mpu6000Config, Pa4, Pb0Pb1Pb4Pb5Low, Pd2ActiveLow, Severity, 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, max = 255) )] system_id: u8, #[config( id = 102, key = "mavlink.component_id", unit = "id", range(min = 1, max = 255) )] component_id: u8, #[config(id = 108, key = "mavlink.logging.enabled", unit = "bool")] logging_enabled: bool, #[config( enum, id = 105, key = "mavlink.logging.maximum_severity", unit = "mav_severity" )] maximum_log_severity: Severity, } #[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(Option); impl ConfigStore for OneRecord { type Error = (); fn load(&mut self) -> Result, 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, logging_enabled: true, maximum_log_severity: Severity::Info, }, telemetry: TelemetryConfig { raw_imu_hz: 20, heartbeat_hz: 2, }, driver: DriverConfig { driver_gain: 7 }, } } struct MalformedProjection { inner: TestUserConfig, system_id: Option, } 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 { 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::(), 10, "runtime projection must retain narrow board-native scalar widths" ); let mut candidate = UserConfigModel::::new(factory()); candidate .set_field(FieldId::new(108), ConfigValue::Bool(false)) .unwrap(); 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!(!runtime.mavlink_logging_enabled); assert_eq!(runtime.mavlink_maximum_log_severity, Severity::Emergency); } #[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::::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::::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)) ); }