Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
240 lines
8.3 KiB
Rust
240 lines
8.3 KiB
Rust
extern crate std;
|
|
use std::vec::Vec;
|
|
|
|
use kaptan_config::{Config, ConfigModel, ConfigValue, FieldId};
|
|
use kaptan_ports::{PortCatalog, PortDescriptor, PortSettings, Ports, Role};
|
|
use platform_stm32f7::{
|
|
f722::{F722Route, Icm42688Imu, ImuService, Mpu6000Imu},
|
|
*,
|
|
};
|
|
|
|
use crate::f722::{config as target_config, geprc, kakute, ports};
|
|
|
|
fn board_route<B, R, Imu>()
|
|
where
|
|
B: F7Board,
|
|
B::ImmutableConfig: F7ImmutableConfig<Route = R>,
|
|
R: F722Route<Imu = Imu>,
|
|
Imu: ImuService,
|
|
{
|
|
}
|
|
|
|
#[test]
|
|
fn board_defaults_disable_configurable_ports() {
|
|
board_route::<kakute::KakuteF7MiniV3, Mpu6000Spi1, Mpu6000Imu>();
|
|
board_route::<geprc::GeprcF722V2, Icm42688Spi3, Icm42688Imu>();
|
|
fn check<B: F7Board>() {
|
|
let candidate =
|
|
target_config::UserConfigModel::<_, B::ImmutableConfig, 5>::new(B::FACTORY_USER_CONFIG);
|
|
let active = candidate.validate(&B::IMMUTABLE_CONFIG).unwrap();
|
|
let inventory = B::IMMUTABLE_CONFIG.f722().hardware.serial();
|
|
assert_eq!(inventory.len(), 5);
|
|
assert_eq!(
|
|
active
|
|
.ports
|
|
.iter()
|
|
.map(|(id, _)| id.get())
|
|
.collect::<Vec<_>>(),
|
|
[1, 2, 3, 4, 6]
|
|
);
|
|
assert!(
|
|
active
|
|
.ports
|
|
.iter()
|
|
.all(|(_, port)| port.role == Role::Disabled)
|
|
);
|
|
let fields: Vec<_> = (0..B::UserConfig::FIELD_COUNT)
|
|
.map(|index| B::UserConfig::descriptor(index).unwrap())
|
|
.collect();
|
|
assert_eq!(fields.len(), 7 + 2 * inventory.len());
|
|
assert_eq!(
|
|
fields[..7]
|
|
.iter()
|
|
.map(|field| field.id.get())
|
|
.collect::<Vec<_>>(),
|
|
[101, 102, 105, 103, 104, 106, 107]
|
|
);
|
|
for (port, fields) in inventory
|
|
.descriptors()
|
|
.iter()
|
|
.zip(fields[7..].chunks_exact(2))
|
|
{
|
|
assert_eq!(fields, port.fields().iter().collect::<Vec<_>>());
|
|
assert_eq!(
|
|
candidate.field_value(port.baud_field),
|
|
Some(ConfigValue::U32(115_200))
|
|
);
|
|
}
|
|
assert_eq!(inventory.index(kaptan_ports::PortId::new(0)), None);
|
|
}
|
|
check::<kakute::KakuteF7MiniV3>();
|
|
check::<geprc::GeprcF722V2>();
|
|
}
|
|
|
|
struct Both;
|
|
impl PortCatalog<3> for Both {
|
|
const DESCRIPTORS: [PortDescriptor; 3] = [UART1, UART2, UART4];
|
|
}
|
|
|
|
#[test]
|
|
fn independent_imu_serial_and_led_facts_allow_cross_route_compositions() {
|
|
let mpu = Mpu6000Spi1 {
|
|
mount: ImuMount::BodyAligned,
|
|
config: Mpu6000Config {
|
|
accelerometer: AccelerometerRange::PlusMinus4G,
|
|
gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond,
|
|
digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz,
|
|
sample_rate_divider: 9,
|
|
},
|
|
};
|
|
let icm = Icm42688Spi3 {
|
|
mount: ImuMount::Yaw180,
|
|
};
|
|
fn check<R: F722Route>(route: R) {
|
|
for led in [StatusLed::ActiveLowPd2, StatusLed::ActiveHighPa13] {
|
|
let inventory = SerialInventory::new(&Both::DESCRIPTORS);
|
|
let hardware =
|
|
F722Hardware::new(route, inventory, led, VerticalSensor::None, MotorPins::Four);
|
|
for sensor in [0, 1, 2] {
|
|
let mut settings = [PortSettings {
|
|
role: Role::Mavlink,
|
|
baud: 115_200,
|
|
}; 3];
|
|
settings[sensor].role = Role::Mtf01;
|
|
let root = Ports::<Both, 3>::new(settings);
|
|
let plan = ports::project_ports::<_, 3>(&root, hardware.serial(), 3400).unwrap();
|
|
assert_eq!(
|
|
plan.iter().filter(|(_, p)| p.role == Role::Mavlink).count(),
|
|
2
|
|
);
|
|
assert_eq!(
|
|
plan.iter().filter(|(_, p)| p.role == Role::Mtf01).count(),
|
|
1
|
|
);
|
|
}
|
|
}
|
|
}
|
|
check(mpu);
|
|
check(icm);
|
|
}
|
|
|
|
#[test]
|
|
fn double_sensor_unsupported_baud_and_missing_inventory_fields_are_rejected() {
|
|
let inventory = SerialInventory::new(&Both::DESCRIPTORS);
|
|
let mut root = Ports::<Both, 3>::new(
|
|
[PortSettings {
|
|
role: Role::Mtf01,
|
|
baud: 115_200,
|
|
}; 3],
|
|
);
|
|
let error = ports::project_ports::<_, 3>(&root, inventory, 0).unwrap_err();
|
|
assert_eq!(error.primary(), Some(UART2.role_field));
|
|
assert_eq!(error.related_field(), Some(UART1.role_field));
|
|
root.set_field(FieldId::new(203), ConfigValue::U8(0))
|
|
.unwrap();
|
|
root.set_field(FieldId::new(207), ConfigValue::U8(0))
|
|
.unwrap();
|
|
assert!(ports::project_ports::<_, 3>(&root, inventory, 0).is_ok());
|
|
assert!(
|
|
root.set_field(FieldId::new(202), ConfigValue::U32(12345))
|
|
.is_err()
|
|
);
|
|
root.set_field(FieldId::new(202), ConfigValue::U32(230400))
|
|
.unwrap();
|
|
assert!(ports::project_ports::<_, 3>(&root, inventory, 0).is_err());
|
|
root.set_field(FieldId::new(201), ConfigValue::U8(1))
|
|
.unwrap();
|
|
assert!(ports::project_ports::<_, 3>(&root, inventory, 23041).is_err());
|
|
assert!(
|
|
ports::project_ports::<_, 3>(&geprc::GeprcF722V2::FACTORY_USER_CONFIG, inventory, 0)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn schema_inventory_and_storage_count_must_agree_even_when_disabled() {
|
|
let root = Ports::<Both, 3>::new(
|
|
[PortSettings {
|
|
role: Role::Disabled,
|
|
baud: 115_200,
|
|
}; 3],
|
|
);
|
|
// No silent unused fields, including fields whose current role is Disabled.
|
|
assert!(ports::project_ports::<_, 1>(&root, SerialInventory::new(&[UART1]), 0).is_err());
|
|
assert!(ports::project_ports::<_, 0>(&root, SerialInventory::new(&[]), 0).is_err());
|
|
assert!(
|
|
ports::project_ports::<_, 2>(&root, SerialInventory::new(&Both::DESCRIPTORS), 0).is_err()
|
|
);
|
|
assert!(
|
|
ports::project_ports::<_, 3>(&root, SerialInventory::new(&[UART1, UART1, UART2]), 0)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_port_descriptors() {
|
|
let root = Ports::<Both, 3>::new(
|
|
[PortSettings {
|
|
role: Role::Disabled,
|
|
baud: 115_200,
|
|
}; 3],
|
|
);
|
|
const DISABLED_ROLES: [(u8, &str); 1] = [Role::CONFIG_ENUM_OPTIONS[0]];
|
|
const INVALID_INVENTORIES: [[PortDescriptor; 3]; 6] = {
|
|
let mut rows = [[UART1, UART2, UART4]; 6];
|
|
rows[0][0].id = kaptan_ports::PortId::new(3);
|
|
rows[1][0].resources = &[8, 9, 101];
|
|
rows[2][0].role_field = FieldId::new(209);
|
|
rows[3][0].role_key = "ports.other.role";
|
|
rows[4][0].allowed_bauds = &[9600];
|
|
rows[5][0].supported_roles = &DISABLED_ROLES;
|
|
rows
|
|
};
|
|
for descriptors in &INVALID_INVENTORIES {
|
|
let inventory = SerialInventory::new(descriptors);
|
|
assert!(ports::project_ports::<_, 3>(&root, inventory, 0).is_err());
|
|
}
|
|
struct WrongMetadata;
|
|
impl PortCatalog<1> for WrongMetadata {
|
|
const DESCRIPTORS: [PortDescriptor; 1] = [PortDescriptor {
|
|
allowed_bauds: &[9600],
|
|
..UART1
|
|
}];
|
|
}
|
|
let root = Ports::<WrongMetadata, 1>::new([PortSettings {
|
|
role: Role::Disabled,
|
|
baud: 9600,
|
|
}]);
|
|
assert!(ports::project_ports::<_, 1>(&root, SerialInventory::new(&[UART1]), 0).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn stable_ids_follow_catalog_order_and_disabled_ports_keep_their_slots() {
|
|
struct Reordered;
|
|
impl PortCatalog<2> for Reordered {
|
|
const DESCRIPTORS: [PortDescriptor; 2] = [UART4, UART1];
|
|
}
|
|
let inventory = SerialInventory::new(&Reordered::DESCRIPTORS);
|
|
let mut root = Ports::<Reordered, 2>::new(
|
|
[PortSettings {
|
|
role: Role::Disabled,
|
|
baud: 115_200,
|
|
}; 2],
|
|
);
|
|
for role in [Role::Disabled, Role::Mavlink, Role::Mtf01] {
|
|
root.set_field(UART4.role_field, ConfigValue::U8(role.wire_value()))
|
|
.unwrap();
|
|
let plan = ports::project_ports::<_, 2>(&root, inventory, 0).unwrap();
|
|
assert_eq!(
|
|
plan.iter().map(|(id, _)| id.get()).collect::<Vec<_>>(),
|
|
[4, 1]
|
|
);
|
|
assert_eq!(plan.get(UART4.id).unwrap().role, role);
|
|
assert_eq!(inventory.index(UART4.id), Some(0));
|
|
assert_eq!(inventory.index(UART1.id), Some(1));
|
|
for id in [0, 2, 3, 5, 255] {
|
|
assert_eq!(inventory.index(kaptan_ports::PortId::new(id)), None);
|
|
}
|
|
}
|
|
}
|