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 [ "configuration: F722ConfigManager", "config_protocol: ConfigProtocol", "receiver: IntegrityMavlinkReceiver<2>", "config_lease_ticks: u16", "wire::decode_command", "self.config_protocol.dispatch_from_composition", "visit_response(response, &mut sender)", ] { 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!( !board.contains("ArmRequest"), "telemetry must not decode arm commands" ); assert!( !board.contains("MAV_CMD_COMPONENT_ARM_DISARM") && !board.contains("decode_arm_request"), "telemetry must not accept an arm command" ); 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_config_interrupt(event)") && !firmware.contains("development-config"), "ordinary images must always dispatch received configuration frames" ); } #[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") && 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", "RAW_IMU_HZ", "HEARTBEAT_HZ", "MAVLINK_MAXIMUM_LOG_SEVERITY", "RANGE_HZ", "OPTICAL_FLOW_HZ", "MAVLINK_LOGGING_ENABLED", ] { 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_eq!( board .matches("self.drain_one_mavlink_log_record();") .count(), 1, "each service turn must make exactly one bounded log admission attempt" ); assert!(board.contains("if runtime.mavlink_logging_enabled")); assert!(board.contains("LogFilter::up_to(runtime.mavlink_maximum_log_severity)")); assert!(board.contains("LogFilter::disabled()")); for periodic_diagnostic_policy in [ "ERROR_RECORDS_PER_SECOND", "LOG_RECORDS_PER_SECOND", "WORST_CASE_OBSERVABILITY_BYTES_PER_SECOND", "KAPTAN_ERROR_FRAME_MAXIMUM_BYTES", "KAPTAN_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"); 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}" ); } }