//! Portable board-support contract for Kaptan. //! //! This crate describes logical board identity, sensing capability and //! outbound data-channel policy. It intentionally has no dependency on an MCU family, //! peripheral access crate, RTIC, or a runtime framework. #![no_std] /// The vertical-sensing data a board support package makes available. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u8)] pub enum VerticalSensingKind { /// No range or optical-flow data is supported. None, /// Range data is supported. Range, /// Coherent range and optical-flow data are supported. RangeAndOpticalFlow, } /// MAVLink endpoint identity selected by one board support package. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MavlinkEndpoint { /// MAVLink system identifier. pub system_id: u8, /// MAVLink component identifier. pub component_id: u8, } /// Fixed outbound data-channel rates driven by a platform service timer. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DataChannelRates { /// Frequency of the timer that calls the data-channel scheduler. pub service_hz: usize, /// Raw-IMU publication rate. pub raw_imu_hz: usize, /// Range publication rate. `None` means range is not published. pub range_hz: Option, /// Optical-flow publication rate. `None` means flow is not published. pub optical_flow_hz: Option, /// Link heartbeat publication rate. pub heartbeat_hz: usize, } /// Static board identity, capability, and data-channel configuration. /// /// The selected platform trait determines the MCU family. Keeping a free-form /// MCU string here would duplicate that type-level fact without validating it. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct BoardConfig { /// Stable board name used in build and diagnostic tooling. pub name: &'static str, /// MAVLink endpoint identity. pub mavlink: MavlinkEndpoint, /// Supported vertical-sensing capability. pub vertical_sensing: VerticalSensingKind, /// Data-channel timer and publication rates. pub data_channel: DataChannelRates, } impl BoardConfig { /// Validates rate bounds and the one-frame-per-service-tick budget. pub const fn validate(self) -> Result<(), BoardConfigError> { let rates = self.data_channel; if rates.service_hz == 0 { return Err(BoardConfigError::ZeroServiceFrequency); } if rates.raw_imu_hz == 0 || rates.heartbeat_hz == 0 { return Err(BoardConfigError::MissingRequiredDataChannel); } if rates.raw_imu_hz > rates.service_hz || rates.heartbeat_hz > rates.service_hz { return Err(BoardConfigError::RateExceedsServiceFrequency); } let Some(mut scheduled_hz) = rates.raw_imu_hz.checked_add(rates.heartbeat_hz) else { return Err(BoardConfigError::DataChannelOvercommitted); }; if let Some(range_hz) = rates.range_hz { if range_hz == 0 || range_hz > rates.service_hz { return Err(BoardConfigError::RateExceedsServiceFrequency); } let Some(total) = scheduled_hz.checked_add(range_hz) else { return Err(BoardConfigError::DataChannelOvercommitted); }; scheduled_hz = total; } if let Some(flow_hz) = rates.optical_flow_hz { if flow_hz == 0 || flow_hz > rates.service_hz { return Err(BoardConfigError::RateExceedsServiceFrequency); } let Some(total) = scheduled_hz.checked_add(flow_hz) else { return Err(BoardConfigError::DataChannelOvercommitted); }; scheduled_hz = total; } if scheduled_hz > rates.service_hz { return Err(BoardConfigError::DataChannelOvercommitted); } match self.vertical_sensing { VerticalSensingKind::None if rates.range_hz.is_none() && rates.optical_flow_hz.is_none() => { Ok(()) } VerticalSensingKind::Range if rates.range_hz.is_some() && rates.optical_flow_hz.is_none() => { Ok(()) } VerticalSensingKind::RangeAndOpticalFlow if rates.range_hz.is_some() && rates.optical_flow_hz.is_some() => { Ok(()) } _ => Err(BoardConfigError::CapabilityDataChannelMismatch), } } } /// Invalid board-support configuration. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BoardConfigError { /// A data-channel schedule needs a nonzero service frequency. ZeroServiceFrequency, /// Raw IMU and heartbeat are mandatory on every flight board. MissingRequiredDataChannel, /// A configured stream is faster than its service timer. RateExceedsServiceFrequency, /// The schedule requests more than one frame per service tick. DataChannelOvercommitted, /// Declared vertical sensing and published streams disagree. CapabilityDataChannelMismatch, } #[cfg(test)] mod tests { use super::{ BoardConfig, BoardConfigError, DataChannelRates, MavlinkEndpoint, VerticalSensingKind, }; const BASE: BoardConfig = BoardConfig { name: "test", mavlink: MavlinkEndpoint { system_id: 1, component_id: 1, }, 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, }, }; #[test] fn accepts_a_schedulable_range_and_flow_board() { assert_eq!(BASE.validate(), Ok(())); } #[test] fn rejects_streams_that_exceed_the_service_budget() { let mut config = BASE; config.data_channel.optical_flow_hz = Some(80); assert_eq!( config.validate(), Err(BoardConfigError::DataChannelOvercommitted) ); } #[test] fn rejects_capability_and_data_channel_mismatch() { let mut config = BASE; config.vertical_sensing = VerticalSensingKind::Range; assert_eq!( config.validate(), Err(BoardConfigError::CapabilityDataChannelMismatch) ); } }