416 lines
16 KiB
Rust
416 lines
16 KiB
Rust
use std::{fs, path::PathBuf};
|
|
|
|
use platform_stm32f7::{
|
|
AccelerometerRange, DigitalLowPassFilter, F722Hardware, F722ImmutableConfig, GyroscopeRange,
|
|
ImuMount, Mavlink, Mpu6000, Mpu6000Config, Pa4, Pb0Pb1Pb4Pb5Low, Pd2ActiveLow, VerticalSensor,
|
|
};
|
|
|
|
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")
|
|
}
|
|
|
|
#[test]
|
|
fn former_motor_pins_are_inhibited_before_all_other_initialization() {
|
|
let module = platform_source("src/f722/mod.rs");
|
|
let interlock = module
|
|
.find("motor_inhibit::inhibit_pb0_pb1_pb4_pb5")
|
|
.expect("F722 initialization must claim the former motor pins first");
|
|
let validation = module
|
|
.find("validate_board_config")
|
|
.expect("F722 initialization must validate its board configuration");
|
|
let board_initialization = module
|
|
.find("board::initialize")
|
|
.expect("F722 initialization must subsequently claim telemetry I/O");
|
|
|
|
assert!(interlock < validation);
|
|
assert!(validation < board_initialization);
|
|
|
|
let interlock = platform_source("src/f722/motor_inhibit.rs");
|
|
let reset = interlock
|
|
.find("port.bsrr()")
|
|
.expect("interlock must reset the former motor-pin output latches");
|
|
let clock_delay = interlock
|
|
.find("rcc.ahb1enr().read()")
|
|
.expect("GPIO access must follow the documented RCC synchronization delay");
|
|
let output_mode = interlock
|
|
.find("port.moder()")
|
|
.expect("interlock must configure the former motor pins as GPIO outputs");
|
|
assert!(
|
|
clock_delay < reset && reset < output_mode,
|
|
"output latches must be low before GPIO output mode is selected"
|
|
);
|
|
for pin in ["br0", "br1", "br4", "br5"] {
|
|
assert!(interlock.contains(pin), "interlock must reset PB pin {pin}");
|
|
}
|
|
for pin in ["moder0", "moder1", "moder4", "moder5"] {
|
|
assert!(
|
|
interlock.contains(pin),
|
|
"interlock must configure PB pin {pin}"
|
|
);
|
|
}
|
|
for pin in ["ot0", "ot1", "ot4", "ot5"] {
|
|
assert!(
|
|
interlock.contains(pin),
|
|
"interlock must set PB pin {pin} push-pull"
|
|
);
|
|
}
|
|
for pin in ["ospeedr0", "ospeedr1", "ospeedr4", "ospeedr5"] {
|
|
assert!(
|
|
interlock.contains(pin),
|
|
"interlock must set PB pin {pin} low speed"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn immutable_board_policy_is_one_complete_literal_manifest() {
|
|
const IMMUTABLE: F722ImmutableConfig = F722ImmutableConfig {
|
|
name: "test",
|
|
service_hz: 100,
|
|
hardware: F722Hardware::new()
|
|
.output_inhibit(Pb0Pb1Pb4Pb5Low)
|
|
.imu(
|
|
Mpu6000::on_spi1()
|
|
.with_drdy(Pa4)
|
|
.mounted(ImuMount::BodyAligned)
|
|
.configured(Mpu6000Config {
|
|
accelerometer: AccelerometerRange::PlusMinus4G,
|
|
gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond,
|
|
digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz,
|
|
sample_rate_divider: 9,
|
|
}),
|
|
)
|
|
.mavlink(Mavlink::on_usart1().baud(115_200))
|
|
.vertical_sensor(VerticalSensor::mtf01_on_uart4().baud(115_200))
|
|
.status_led(Pd2ActiveLow)
|
|
.telemetry_only(),
|
|
};
|
|
|
|
assert_eq!(IMMUTABLE.name, "test");
|
|
assert_eq!(IMMUTABLE.service_hz, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn telemetry_platform_has_no_physical_actuator_peripheral_path() {
|
|
let sources = [
|
|
platform_source("src/lib.rs"),
|
|
platform_source("src/f722/mod.rs"),
|
|
platform_source("src/f722/board.rs"),
|
|
platform_source("src/f722/firmware.rs"),
|
|
platform_source("src/f722/interrupts.rs"),
|
|
]
|
|
.join("\n");
|
|
|
|
for forbidden in [
|
|
"mod actuator",
|
|
"MotorOutput",
|
|
"MotorRoute",
|
|
"MotorOutputs",
|
|
"TIM3",
|
|
"TIM5",
|
|
"Dshot",
|
|
"Pwm",
|
|
"device.TIM3",
|
|
"device.TIM5",
|
|
] {
|
|
assert!(
|
|
!sources.contains(forbidden),
|
|
"telemetry platform must not expose actuator path {forbidden}"
|
|
);
|
|
}
|
|
|
|
let manifest = platform_source("Cargo.toml");
|
|
assert!(
|
|
!manifest.contains("kaptan-actuator"),
|
|
"telemetry platform must not link the physical actuator crate"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_f722_image_has_direct_configuration_after_output_inhibition() {
|
|
let board = platform_source("src/f722/board.rs");
|
|
let firmware = platform_source("src/f722/firmware.rs");
|
|
let routes = platform_source("src/f722/routes.rs");
|
|
let manifest = platform_source("Cargo.toml");
|
|
|
|
assert!(
|
|
!manifest.contains("development-config")
|
|
&& !manifest.contains("development-authority")
|
|
&& !manifest.contains("development-unsigned"),
|
|
"ordinary board images must not hide configuration behind a development feature"
|
|
);
|
|
for required in [
|
|
"config_file: F722ConfigFile",
|
|
"file_service: FileService",
|
|
"uart_receiver: IntegrityMavlinkReceiver<1>",
|
|
"file_lease_ticks: u16",
|
|
"TRANSFER_REPLAY_LEASE_SECONDS",
|
|
"let Some(message) = frame.decode()",
|
|
"wire::decode_request",
|
|
".dispatch(&mut self.config_file",
|
|
"send_file_response",
|
|
"let class = kaptan_mavlink::transmitter::FrameClass::Critical",
|
|
"control.status != FileStatus::RequestConflict",
|
|
] {
|
|
assert!(
|
|
board.contains(required),
|
|
"ordinary image must retain configuration composition: {required}"
|
|
);
|
|
}
|
|
assert!(!board.contains("DirectConfigAdmission"));
|
|
assert!(
|
|
routes.contains("pub(crate) struct Mpu6000Spi1Route {\n spi: Spi1,\n}")
|
|
&& routes.contains("stm32f722::GPIOB::ptr()")
|
|
&& routes.contains("write.br2().set_bit()")
|
|
&& routes.contains("write.bs2().set_bit()"),
|
|
"the MPU runtime may drive only PB2 atomically, not retain GPIOB ownership"
|
|
);
|
|
assert!(!board.contains("development-config"));
|
|
assert!(
|
|
!firmware.contains("shared = [mavlink, motors]"),
|
|
"the MAVLink worker must not share a motor resource"
|
|
);
|
|
assert!(
|
|
firmware.contains("const MAVLINK_EVENT_RECEIVED")
|
|
&& firmware.contains("mavlink.receive_mavlink_interrupt(event)")
|
|
&& !firmware.contains("development-config"),
|
|
"ordinary images must always dispatch received MAVLink frames"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn flash_busy_deadline_resets_entirely_from_sram() {
|
|
let config = platform_source("src/f722/config.rs");
|
|
assert!(config.contains("link_section = \".data.kaptan_flash_operation\""));
|
|
assert!(config.contains("fn execute(&self, operation: F722FlashOperation)"));
|
|
assert!(config.contains("\"mrs {primask}, PRIMASK\""));
|
|
assert!(config.contains("\"cpsid i\""));
|
|
assert!(config.contains("\"cpsie i\""));
|
|
assert!(config.contains("fn reset_after_flash_timeout() -> !"));
|
|
assert!(config.contains("SYSRESETREQ"));
|
|
assert!(config.contains("core::arch::asm!(\"dsb sy\""));
|
|
assert!(!config.contains("BusyTimeout"));
|
|
}
|
|
|
|
#[test]
|
|
fn usart_tx_dma_arms_each_stream_before_its_request() {
|
|
let usart = platform_source("src/f722/peripherals/usart.rs");
|
|
assert!(usart.contains("disable_dma_transmit_from_interrupt"));
|
|
|
|
for (name, path) in [
|
|
("USART1", "src/f722/peripherals/usart1_tx_dma2_stream7.rs"),
|
|
("USART2", "src/f722/peripherals/usart2_tx_dma1_stream6.rs"),
|
|
] {
|
|
let route = platform_source(path);
|
|
let constructor_start = route.find("pub fn new").expect("DMA constructor");
|
|
let transfer_start = route.find("pub fn start").expect("DMA start operation");
|
|
let acknowledge_start = route
|
|
.find("pub fn acknowledge_interrupt")
|
|
.expect("DMA interrupt acknowledgement");
|
|
|
|
assert!(
|
|
!route[constructor_start..transfer_start].contains("enable_dma_transmit"),
|
|
"{name} must not leave its DMA request armed between transfers"
|
|
);
|
|
|
|
assert!(
|
|
route.contains("write.dmdis().enabled().feie().disabled()"),
|
|
"{name} must explicitly select direct mode without FE interrupts"
|
|
);
|
|
let start = &route[transfer_start..acknowledge_start];
|
|
let stream_enable = start
|
|
.find("write.en().enabled()")
|
|
.expect("DMA stream must be enabled");
|
|
let request_enable = start
|
|
.find("self.usart.enable_dma_transmit()")
|
|
.expect("USART DMA request must be enabled");
|
|
assert!(
|
|
stream_enable < request_enable,
|
|
"{name} must enable its stream before the peripheral request"
|
|
);
|
|
|
|
let acknowledge = &route[acknowledge_start..];
|
|
let stream_disable = acknowledge
|
|
.find("write.en().disabled()")
|
|
.expect("DMA stream must be disabled");
|
|
let request_disable = acknowledge
|
|
.find("disable_dma_transmit_from_interrupt()")
|
|
.expect("USART DMA request must be disabled");
|
|
assert!(
|
|
stream_disable < request_disable,
|
|
"{name} must disable its stream before the peripheral request"
|
|
);
|
|
|
|
let classifier = acknowledge
|
|
.find("impl DmaTerminalStatus")
|
|
.map(|index| &acknowledge[index..])
|
|
.expect("DMA terminal classifier");
|
|
assert!(classifier.contains("self.transfer_error || self.direct_mode_error"));
|
|
assert!(classifier.contains("TxCompletion::Complete"));
|
|
assert!(
|
|
!classifier.contains("fifo_error"),
|
|
"{name} direct-mode FEIF must not manufacture a terminal fault"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn configuration_schema_is_generated_without_board_forwarding_code() {
|
|
let platform = platform_source("src/lib.rs");
|
|
let platform_config = platform_source("src/f722/config.rs");
|
|
let platform_firmware = platform_source("src/f722/firmware.rs");
|
|
let kakute = platform_source("../../boards/kakute-f7-mini-v3/src/board.rs");
|
|
let geprc = platform_source("../../boards/geprc-f722-v2/src/board.rs");
|
|
|
|
assert!(
|
|
platform.contains("pub struct F722ImmutableConfig")
|
|
&& platform.contains("pub hardware: F722Hardware")
|
|
&& platform.contains("type UserConfig: Clone + Config")
|
|
&& platform_config.contains("struct UserConfigModel")
|
|
&& platform_config.contains("fn project_runtime<C, I>")
|
|
&& platform_config.contains("candidate.field_value(field)"),
|
|
"the platform must retain exactly one immutable manifest and one generic user adapter"
|
|
);
|
|
for semantic_id in [
|
|
"MAVLINK_SYSTEM_ID",
|
|
"MAVLINK_COMPONENT_ID",
|
|
"IMU_HZ",
|
|
"HEARTBEAT_HZ",
|
|
"MAVLINK_LOG_LEVEL_FILTER",
|
|
"RANGE_HZ",
|
|
"OPTICAL_FLOW_HZ",
|
|
] {
|
|
assert!(
|
|
platform_config.contains(&format!("const {semantic_id}: FieldId")),
|
|
"missing stable platform-consumed semantic ID {semantic_id}"
|
|
);
|
|
}
|
|
for (name, board) in [("Kakute", kakute), ("GEPRC", geprc)] {
|
|
assert!(
|
|
board.contains("type UserConfig = UserConfig;")
|
|
&& board.contains("type ImmutableConfig = ImmutableConfig;")
|
|
&& board.contains("const FACTORY_USER_CONFIG: UserConfig = UserConfig {")
|
|
&& board.contains("const IMMUTABLE_CONFIG: ImmutableConfig = ImmutableConfig {")
|
|
&& board.contains("hardware: F722Hardware::new()")
|
|
&& board.contains(".telemetry_only()"),
|
|
"{name} must visibly declare the two literal roots"
|
|
);
|
|
for redundant in [
|
|
"impl F7ImmutableConfig for",
|
|
"impl F7UserConfig for",
|
|
"impl_f722_user_config!",
|
|
] {
|
|
assert!(!board.contains(redundant), "{name} retains {redundant}");
|
|
}
|
|
}
|
|
assert!(
|
|
platform_firmware.contains("<$board as $crate::F7Board>::UserConfig")
|
|
&& platform_firmware.contains("<$board as $crate::F7Board>::ImmutableConfig")
|
|
&& platform_firmware.contains("config_snapshot_length::<"),
|
|
"the common RTIC graph must specialize storage for the two board roots"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn f722_link_budget_reserves_only_deterministic_telemetry() {
|
|
let board = platform_source("src/f722/board.rs");
|
|
assert!(board.contains("SCHEDULED_TELEMETRY_BYTES_PER_SECOND"));
|
|
assert!(board.contains("SERVICE_HZ * SCHEDULED_FRAME_MAXIMUM_BYTES"));
|
|
assert!(board.contains("self.drain_one_mavlink_diagnostic();"));
|
|
assert!(board.contains("let mavlink_log_filter = runtime.mavlink_log_level_filter;"));
|
|
assert!(board.contains("LogSinkQueue::new(mavlink_log_filter)"));
|
|
assert!(!board.contains("mavlink_logging_enabled"));
|
|
assert!(!board.contains("LogFilter"));
|
|
for periodic_diagnostic_policy in [
|
|
"ERROR_RECORDS_PER_SECOND",
|
|
"LOG_RECORDS_PER_SECOND",
|
|
"WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND",
|
|
"ERROR_FRAME_MAXIMUM_BYTES",
|
|
"LOG_FRAME_MAXIMUM_BYTES",
|
|
"budget_ticks",
|
|
"error_budget",
|
|
"log_budget",
|
|
] {
|
|
assert!(
|
|
!board.contains(periodic_diagnostic_policy),
|
|
"diagnostics must not have a periodic policy: {periodic_diagnostic_policy}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mavlink_transport_faults_do_not_recurse_or_retain_dead_accounting() {
|
|
let board = platform_source("src/f722/board.rs");
|
|
let transport = platform_source("src/f722/data_channel.rs");
|
|
let portable_channel = platform_source("../../libs/kaptan-data-channel/src/lib.rs");
|
|
|
|
assert!(
|
|
!board.contains("mavlink transmit dma fault"),
|
|
"the MAVLink sink must not report its own transmit failure through itself"
|
|
);
|
|
|
|
for recursive_path in [
|
|
"drain_transport_faults",
|
|
"take_transport_fault",
|
|
"TransmitMemory",
|
|
"TransmitEngine",
|
|
"TransmitTerminal",
|
|
"pending_faults",
|
|
] {
|
|
assert!(
|
|
!board.contains(recursive_path)
|
|
&& !transport.contains(recursive_path)
|
|
&& !portable_channel.contains(recursive_path),
|
|
"MAVLink transport must not feed {recursive_path} back into itself"
|
|
);
|
|
}
|
|
for dead_counter in [
|
|
"dma_fault_count",
|
|
"memory_fault_count",
|
|
"engine_fault_count",
|
|
"terminal_fault_count",
|
|
] {
|
|
assert!(
|
|
!transport.contains(dead_counter),
|
|
"unused transport accounting must stay absent: {dead_counter}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn f722_producers_keep_logs_and_reportable_errors_separate() {
|
|
let board = platform_source("src/f722/board.rs");
|
|
let config = platform_source("src/f722/config.rs");
|
|
let logger = platform_source("../../libs/kaptan-log/src/facade.rs");
|
|
let errors = platform_source("../../libs/kaptan-err-reportable/src/ingress.rs");
|
|
|
|
for macro_name in [
|
|
"kaptan_log::debug!",
|
|
"kaptan_log::info!",
|
|
"kaptan_log::warn!",
|
|
"kaptan_log::error!",
|
|
] {
|
|
assert!(
|
|
board.contains(macro_name),
|
|
"f722 does not exercise {macro_name}"
|
|
);
|
|
}
|
|
assert!(board.contains("#[kaptan_err_reportable::propagate]"));
|
|
assert!(
|
|
config.contains("enum PersistConfigurationError {")
|
|
&& config.contains("#[kaptan_error(id = 2210)]")
|
|
&& config.contains("#[kaptan_error(id = 2209)]")
|
|
&& config.contains("impl From<JournalError<F722FlashError>>")
|
|
&& config.contains("fn persist_candidate(")
|
|
&& config.contains("#[kaptan_err_reportable::propagate]")
|
|
);
|
|
assert!(!board.contains("report_error"));
|
|
assert!(!board.contains("report_static_error"));
|
|
assert!(logger.contains("GLOBAL_INGRESS_CAPACITY"));
|
|
assert!(logger.contains("compare_exchange"));
|
|
assert!(errors.contains("AtomicU8"));
|
|
assert!(errors.contains("UnsafeCell<MaybeUninit<ErrorRecord>>"));
|
|
assert!(errors.contains("compare_exchange"));
|
|
}
|