feat(f722): enforce board policy and emit diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-09 22:08:03 +03:00
1 parent a4ef0f0740
commit f4f47d7302
16 files changed
+580 -179

No files matched your search

@@ -1,5 +1,16 @@
use std::{fs, path::PathBuf};
use kaptan_board::{
BoardConfig, BoardProfile, DataChannelRates, ImuDevice, ImuMount, MavlinkEndpoint, MavlinkPort,
TelemetryProfile, VerticalSensingKind,
};
use kaptan_log::{LogFlags, LogRecord, LogText, Severity};
use kaptan_log_mavlink::{DiagnosticPayload, encode_diagnostic};
use kaptan_mavlink::encoder::MavlinkEncoder;
use platform_stm32f7::{
BoardError, F7BoardBuilder, Mavlink, Mpu6000, Pa4, VerticalSensor, validate_board_config,
};
fn platform_source(relative: &str) -> String {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fs::read_to_string(root.join(relative)).expect("platform source must exist")
@@ -12,7 +23,7 @@ fn former_motor_pins_are_inhibited_before_all_other_initialization() {
.find("motor_inhibit::inhibit_motor_pins")
.expect("F722 initialization must claim the former motor pins first");
let validation = module
.find("config.validate()")
.find("validate_board_config")
.expect("F722 initialization must validate its board configuration");
let board_initialization = module
.find("board::initialize")
@@ -55,6 +66,87 @@ fn former_motor_pins_are_inhibited_before_all_other_initialization() {
}
}
fn mpu_config() -> BoardConfig {
BoardConfig {
name: "test",
mavlink: MavlinkEndpoint {
system_id: 1,
component_id: 1,
port: MavlinkPort::Serial1,
baud: 115_200,
},
profile: BoardProfile::TelemetryOnly(TelemetryProfile {
imu: ImuDevice::Mpu6000,
imu_mount: ImuMount::BodyAligned,
vertical_sensing: VerticalSensingKind::RangeAndOpticalFlow,
data_channel: DataChannelRates {
service_hz: 100,
raw_imu_hz: 20,
range_hz: Some(20),
optical_flow_hz: Some(20),
heartbeat_hz: 2,
},
maximum_log_level: Severity::Info,
}),
}
}
#[test]
fn immutable_board_policy_must_match_the_executed_route_graph() {
let routes = F7BoardBuilder::new()
.imu(Mpu6000::on_spi1().with_drdy(Pa4))
.mavlink(Mavlink::on_usart1())
.vertical_sensor(VerticalSensor::mtf01_on_uart4())
.build()
.unwrap();
assert_eq!(validate_board_config(mpu_config(), routes), Ok(()));
let mut wrong_imu = mpu_config();
let BoardProfile::TelemetryOnly(profile) = &mut wrong_imu.profile else {
unreachable!();
};
profile.imu = ImuDevice::Icm42688;
assert_eq!(
validate_board_config(wrong_imu, routes),
Err(BoardError::ImuRouteMismatch)
);
let mut wrong_mount = mpu_config();
let BoardProfile::TelemetryOnly(profile) = &mut wrong_mount.profile else {
unreachable!();
};
profile.imu_mount = ImuMount::Yaw180;
assert_eq!(
validate_board_config(wrong_mount, routes),
Err(BoardError::ImuMountMismatch)
);
let mut wrong_vertical = mpu_config();
let BoardProfile::TelemetryOnly(profile) = &mut wrong_vertical.profile else {
unreachable!();
};
profile.vertical_sensing = VerticalSensingKind::Range;
profile.data_channel.optical_flow_hz = None;
assert_eq!(
validate_board_config(wrong_vertical, routes),
Err(BoardError::VerticalSensorCapabilityMismatch)
);
let mut wrong_port = mpu_config();
wrong_port.mavlink.port = MavlinkPort::Serial2;
assert_eq!(
validate_board_config(wrong_port, routes),
Err(BoardError::MavlinkRouteMismatch)
);
let mut overcommitted_link = mpu_config();
overcommitted_link.mavlink.baud = 9_600;
assert_eq!(
validate_board_config(overcommitted_link, routes),
Err(BoardError::TelemetryLinkOvercommitted)
);
}
#[test]
fn telemetry_platform_has_no_physical_actuator_peripheral_path() {
let sources = [
@@ -117,3 +209,38 @@ fn telemetry_has_no_inbound_command_authority() {
"telemetry must not schedule a worker from an inbound command"
);
}
#[test]
fn f722_link_budget_uses_both_maximum_generated_diagnostic_frames() {
let text = LogText::from_message(&"x".repeat(160));
let log_record = LogRecord::new(Severity::Info, LogFlags::NONE, text);
let error_record = LogRecord::new(Severity::Error, LogFlags::NONE, text);
let mut encoder = MavlinkEncoder::new(1, 1);
let DiagnosticPayload::Log(log_payload) = encode_diagnostic(&log_record) else {
panic!("info record must use KAPTAN_LOG");
};
let log_frame = encoder.custom(&log_payload);
let DiagnosticPayload::Error(error_payload) = encode_diagnostic(&error_record) else {
panic!("error record must use KAPTAN_ERROR");
};
let error_frame = encoder.custom(&error_payload);
assert_eq!(log_frame.raw_bytes().len(), 175);
assert_eq!(
kaptan_mavlink::message::kaptan::KAPTAN_LOG_DATA::ENCODED_LEN,
163
);
assert_eq!(error_frame.raw_bytes().len(), 174);
assert_eq!(
kaptan_mavlink::message::kaptan::KAPTAN_ERROR_DATA::ENCODED_LEN,
162
);
assert_eq!(
62 * 48 + 10 * error_frame.raw_bytes().len() + 5 * log_frame.raw_bytes().len(),
5_591
);
let board = platform_source("src/f722/board.rs");
assert!(board.contains("KAPTAN_ERROR_FRAME_MAXIMUM_BYTES"));
assert!(board.contains("KAPTAN_LOG_FRAME_MAXIMUM_BYTES"));
assert!(board.contains("5,591 bytes/s"));
}