feat(firmware): validate configuration and migrate sensor-driven flight APIs
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.
This commit is contained in:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -0,0 +1,239 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user