234 lines
6.8 KiB
Rust
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))
|
|
);
|
|
}
|